When we build software we’re not always guaranteed to have a happy path, there are things that we can’t control, even if you have controlled “everything” there are things you don’t have access to, for instance, network availability, hardware capabilities, and many others. so those unexpected events in programming are called “Exceptions”.
So as a simple definition we can say:
Exceptions are our way of saying that the normal execution of a program has been failed because of an unexpected cause, which can be as simple as providing invalid input value or hardware-related error for instance losing network connectivity.
almost all programming languages provide a native approach to work with Exception, in this article, we will talk about how we can work with Exceptions in the context of the .NET framework.
Exceptions & .NET
In .NET we are given 3 components to work with exceptions:
- The Exception class
- The Throw keyword
- The Try/catch(finally) block
we only need these 3 components to do all the tasks related to exceptions, we can create exceptions, throw them and handle them.
so how do exceptions work?
when we encounter an exceptional event in our program we will have an exception thrown. exceptions can either be thrown by us or by the .NET runtime, simply by using the throw keyword, then once the exception is thrown the execution of the code is stopped and the exception will be propagated up the call stack until a catch statement is found. If the exception is not caught (with a try/catch block) a generic exception handler will be used to handle the exception (based on the environment), and the program will be terminated.
Exception Types
Exceptions are just simple .NET classes that derive from the “Exception” class, you can create your own type, or use pre-built types given to you by the .NET framework.
Built in types
the built-in types are predefined exception classes in the .NET Framework to represent generic & specific errors, you can use them to indicate unexpected errors in your apps without the need for creating custom ones for example we have:
- ArgumentNullException: is used to indicate that a given argument is null.
- ArgumentOutOfRangeException: is used to indicate that the given arguement is out of the expected range.
- FileNotFoundException: is used to indicate a failed attempt to access a non-existing file on disk.
and there are more other types to choose from, you just need to select the right type for your case.
Custom types
what if you want to indicate a failure/error for a custom use case that the built-in types are not enough to represent, This is where custom types come in handy. custom exception types are just simple C# classes that inherit from the Exception base class.
so let’s see how can we create our own exception type, let’s say we have a function that loads an email template by Id, and if we can’t locate the email template we need to throw a custom exception EmailTemplateNotFoundException. so we will create a class that looks like this:
[Serializable]
public class EmailTemplateNotFoundException : Exception
{
public EmailTemplateNotFoundException(string templateId)
: base($"there is no template with id: [templateId]") { }
protected EmailTemplateNotFoundException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
}
Quick tip
When creating a custom exception type, use the snippet code inside Visual Studio IDE, just type Exception and click on Tab, then a predefined template will be given to you.
as you can see the class we have created is named EmailTemplateNotFoundException, which follows a naming convention. exception names are preferred to be ended with a suffix of “Exception”.
also, we have added a constructor that takes the templateId as an argument and provides a default exception message, here we have the freedom to do whatever we want, add default values, define custom properties, and more. the second constructor is used by the serialization API so it is a good practice to add this constructor with the Serializable attribute.
Exception usage
so now we have our exception types ready how can we use them, let’s stick with the previous example. our function will look like this.
static void SendEmailMessage(string userEmail, string templateId)
{
var template = LoadEmailTemplate(templateId);
if (template is null)
throw new EmailTemplateNotFoundException(templateId);
// other code
}
static string? LoadEmailTemplate(string templateId)
{
// here we will have our logic for
// retrieving the email template
return null;
}
here we have 2 functions one is used to load the email template by id, and another one that uses that template, now if the template is null this means we couldn’t load it, so we need to report this unexpected error using exceptions.
so what we have done here is first we initialize an instance of the exception type with the new keyword. then we used the throw keyword to throw the exception, with that our job is done and everything else will be handled by the runtime. the execution of the code will be stopped and exceptions will be propagated up the call stack.
Exception handling
now we have our exceptions thrown, how can we handle them?
It actually pretty simple to handle exceptions, you just need to put your code inside a try/catch block and you’re done.
Try/Catch block
here is how to define a try/catch block:
try
{
SendEmailMessage("user_id", "template_id");
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
what if you want to catch specific exception types, for instance, our EmailTemplateNotFoundException, we just need to add another catch block with the exception type we want.
try
{
SendEmailMessage("user_id", "template_id");
}
catch(EmailTemplateNotFoundException ex)
{
Console.WriteLine(ex.Message);
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
Attention
the order of the catch statements should respect the inheritance hierarchy,
Quick tip
for a more in-depth explanation of the Try/Catch block with all the possible use cases, check out the Microsoft docs, they have an excellent guide.
Try/Catch/finally block
the try/catch block can be extended with a “finally” block, which is useful to clean up resources, it can be used as follow:
var file = new System.IO.StreamReader(@"c:\test_file.txt");
try
{
SendEmailMessage("user_id", "template_id");
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
if (file is not null)
file.close();
}
the “finally” block is always greeted to run (there are cases where it is not) after the catch block is executed.
you can check the docs for more details on the try/catch/finally block.
Global Handlers
the global handlers are a great option to centralize the exceptions handling in one place, but it is not always a good practice to use them. it is all about your app logic, project type, your requirement, etc, which will define when to use them & when not.
so how do we create these global handlers?
each project type has its own way of defining the global exception handlers, for example in the .NET framework Winforms project we use the AppDomain to register an event handler that will be called for all unhandled exceptions.
public static void Main()
{
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(GlobalyExceptionHandler);
// other code
}
static void GlobalyExceptionHandler(object sender, UnhandledExceptionEventArgs args)
{
var exception = (Exception) args.ExceptionObject;
Console.WriteLine("global hanlder, exception message: " + exception.Message);
}
and for ASP core projects you can use the ExceptionHandler Middleware to catch the exceptions like so:
app.UseExceptionHandler(exceptionHandler =>
{
exceptionHandler.Run(async context =>
{
// set the status code & content type
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = Text.Plain;
// write a plain text response
await context.Response.WriteAsync("An exception was thrown.");
});
});
and same with WPF, and the other projects, you just need to look at how to handle exceptions globally in your app project type.
but as I said using global handlers is not always a good practice, in some cases, they may be very handy, for example in ASP Core API projects they are extremely useful. for the other project types, you may use them to log the errors for future debugging.
Conclusion
Exceptions are used to indicate unexpected errors in our apps where we need to guarantee a clear execution path, the .NET framework has given us a simple way of working with Exceptions. and as they say with great power comes great responsibility, use exceptions carefully so that it doesn’t hurt the performance of your app, check out the other patterns for error handling like Result objects (To Throw or To Return (Exceptions vs Result Object)?).
I hope you enjoyed this article if you have any questions or suggestions just leave a comment and I will answer you.