An integral part of developers' work is fixing bugs in the application. It is not so easy to predict all scenarios of how users will work with your application. That's why sometimes (or maybe even often) they report errors (which may not necessarily be the fault of the programmer). If a user informs you that there is an error in the application, without detailed information, it may be difficult for you to fix the error. Sometimes it is even difficult to trigger this error again. Of course, in some situations, if it is not a critical error, even the user may not inform you about this error. However, we always lose a bit in the eyes of our customers. Therefore, an extremely important element is appropriate exception capture and handling. You will learn how to do it correctly from this article.
Example
I have prepared a short code in C#. This is a console application that calls the method responsible for sending emails in the static Main method. The Send method should perform the logic responsible for sending the email. In the body of the Send method, the Connect method is also called on the HostSmtp class object, which in our case only simulates an error with the Cannot connect message.
using System;
namespace App
{
public class HostSmtp
{
public void Connect()
{
throw new Exception("Cannot connect.");
}
}
public class EmailSender
{
public void Send()
{
new HostSmtp().Connect();
}
}
public class Program
{
static void Main(string[] args)
{
new EmailSender().Send();
}
}
}
After running this program, we get the following result in the console:
Unhandled Exception: System.Exception: Cannot connect.
at App.HostSmtp.Connect() in C:\ConsoleApp\Program.cs:line 9
at App.EmailSender.Send() in C:\ConsoleApp\Program.cs:line 17
at App.Program.Main(String[] args) in C:\ConsoleApp\Program.cs:line 25
This was not a surprise, such a situation could have been predicted. You must admit, however, that this behavior of the application is, firstly, not very user-friendly, and secondly, it does not notify the administrator about any unexpected behavior of the application. Therefore, if there is some code where we expect to receive an error, we can call that code in a try catch block.
Is using a try catch block enough?
Thanks to the use of the try catch block, our call to the Send method may look like this:
public class EmailSender
{
public void Send()
{
try
{
new HostSmtp().Connect();
}
catch (Exception)
{
}
}
}
Ok, now we restart our application. There are no errors, so I guess everything is fine? Well, not entirely :) Actually, this solution is even worse than the previous one, because we caught the exception, but it was not properly handled by us. The email that was supposed to be sent was not sent and we were not informed about this fact. So, this solution is terrible. Try to avoid such a situation, there is never a good reason to use such a notation. So how to handle this exception? We have 2 problems here that we need to solve. First, we want the system administrator to be able to view detailed information about the error and to display a more user-friendly message to the user, in particular without information about the entire error stack.
Saving errors
The first problem can be easily solved by simply using one of the many data logging frameworks and recording all the detailed information about the error so that the cause of the error can be easily diagnosed. This code might look like this:
public class EmailSender
{
public void Send()
{
try
{
new HostSmtp().Connect();
}
catch (Exception ex)
{
//Save all detailed error information
Logger.Error("dodatkowe-informacje-o-błędzie", ex);
}
}
}
We managed to save all the detailed information about the error to the file, it's better, but we still have the problem that the error was caught but not handled properly. Further, the user does not know that the operation failed.
Popular ways of handling exceptions
There are many ways to handle this error. Let's look at the 4 most commonly used solutions and then choose the best ones.
1 way "throw ex":
public class EmailSender
{
public void Send()
{
try
{
new HostSmtp().Connect();
}
catch (Exception ex)
{
//Save all detailed error information
Logger.Error("dodatkowe-informacje-o-błędzie", ex);
throw ex;
}
}
}
Result:
Unhandled Exception: System.Exception: Cannot connect.
at App.EmailSender.Send() in C:\ConsoleApp\Program.cs:line 25
at App.Program.Main(String[] args) in C:\ConsoleApp\Program.cs:line 34
Comments:
Unfortunately, using throw ex; we do not have full information about the error that occurred. There is no information that an exception was thrown in the Connect method on line 9, so diagnosing the error may be more difficult. This way is wrong.
2 way "throw new Exception("Some exception.")":
public class EmailSender
{
public void Send()
{
try
{
new HostSmtp().Connect();
}
catch (Exception ex)
{
//Save all detailed error information
Logger.Error("dodatkowe-informacje-o-błędzie", ex);
throw new Exception("Some exception.");
}
}
}
Result:
Unhandled Exception: System.Exception: Some exception.
at App.EmailSender.Send() in C:\ConsoleApp\Program.cs:line 25
at App.Program.Main(String[] args) in C:\ConsoleApp\Program.cs:line 34
Comments:
As in the previous example, when we use throw new Exception, we lose information about the error that occurred earlier. So, this way is also wrong.
3 way "throw":
public class EmailSender
{
public void Send()
{
try
{
new HostSmtp().Connect();
}
catch (Exception ex)
{
//Save all detailed error information
Logger.Error("dodatkowe-informacje-o-błędzie", ex);
throw;
}
}
}
Result:
Unhandled Exception: System.Exception: Cannot connect.
at App.HostSmtp.Connect() in C:\ConsoleApp\Program.cs:line 9
at App.EmailSender.Send() in C:\ConsoleApp\Program.cs:line 25
at App.Program.Main(String[] args) in C:\ConsoleApp\Program.cs:line 34
Comments:
And that's what we wanted. If only throw is used, we have the entire stack trace, all information about previous errors. As you can see, only in this case, the information about the first error that occurred in the Connect method on line 9 was not lost. So, of these 3 methods, often found in various applications, only this method is correct.
4 way "throw new Exception("Some exception.", ex)":
public class EmailSender
{
public void Send()
{
try
{
new HostSmtp().Connect();
}
catch (Exception ex)
{
//Save all detailed error information
Logger.Error("dodatkowe-informacje-o-błędzie", ex);
throw new Exception("Some exception.", ex);
}
}
}
Result:
Unhandled Exception: System.Exception: Some exception. ---> System.Exception: Cannot connect.
at App.HostSmtp.Connect() in C:\ConsoleApp\Program.cs:line 9
at App.EmailSender.Send() in C:\ConsoleApp\Program.cs:line 19
--- End of inner exception stack trace ---
at App.EmailSender.Send() in C:\ConsoleApp\Program.cs:line 25
at App.Program.Main(String[] args) in C:\ConsoleApp\Program.cs:line 34
Comments:
As you can see, there is a 4th way to handle exceptions. This method is also good, but it is usually used only when we want to throw a different type of exception.
Displaying a message to the user
Of course, we cannot display such an error to the user, we should display an appropriate message, which is most often done in the global method, which looks different depending on the type of application. In our case, in a console application it may look like this:
public class Program
{
static void Main(string[] args)
{
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
new EmailSender().Send();
}
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Logger.Error(ex. ExceptionObject);
Console.WriteLine("An unhandled error occurred.");
Environment.Exit(1);
}
}
Or you can handle a direct call:
public class Program
{
static void Main(string[] args)
{
try
{
new EmailSender().Send();
}
catch (Exception ex)
{
Console.WriteLine("Failed to send email.");
}
}
}
Thanks to this solution, the application continues to work properly and the user is informed about the error. I've displayed a general error here, but you can also display more detailed information using the Message property of each exception.
So, the whole code might look like this:
using System;
namespace App
{
public class HostSmtp
{
public void Connect()
{
throw new Exception("Cannot connect.");
}
}
public class EmailSender
{
public void Send()
{
try
{
new HostSmtp().Connect();
}
catch (Exception ex)
{
//Save all detailed error information
Logger.Error("additional-error-info", ex);
throw;
}
}
}
public class Program
{
static void Main(string[] args)
{
try
{
new EmailSender().Send();
}
catch (Exception ex)
{
Console.WriteLine("Failed to send email.");
}
}
}
}
SUMMARY:
I hope that in this article I was able to show you how to properly handle exceptions in C#. Remember to never leave a catch clause empty, because then the error will be suppressed and not handled in any way. You should always record as much information about errors as possible, which will make detecting and correcting them much easier. This will save you a lot of time in the future. I showed you several ways to properly handle such exceptions. Note that the best way is usually to just use throw, so that no information about the previous error is lost.