Exception Handling is vital for applications of all types and traffic volumes. If exceptions are not handled well within the application, it may break the entire application or even lead to data loss. In ASP.NET Core, there are multiple ways that one can handle exceptions effectively. I have included this article as part of my ongoing .NET Zero to Hero Series
as this is an important aspect for developers learning about building production-ready .NET applications.
In this article, we will focus on handling exceptions globally in an ASP.NET Core application, so that there is a central error-handling mechanism throughout your applications. This makes things quite easy and manageable.
Exceptions in .NET
Exceptions in .NET are objects that inherit from the System.Exception
base class and can be thrown from the part of your code base wherever the problem has occurred.
Some common exceptions that you often encounter while working with .NET are NullReferenceException, ArgumentNullException, IndexOutOfRangeException, etc.
Getting started with Error Handling in ASP.NET Core
For this demonstration, we will be working on a new ASP.NET Core Web API (.NET 8) project and Visual Studio 2022 as my default IDE.
Here are a few ways how you can handle exceptions /errors in your ASP.NET Core Applications:
- Traditional Try Catch blocks
- Built-In Exception Handling Middleware
- Custom Middleware to Handle Exceptions
- All New
IExceptionHandler
[Highly Recommended] - Starting from .NET 8
We will go through each of these mechanisms, but our main focus and recommendation would be to use the IExceptionHandler
feature that got introduced from .NET 8!
Try Catch Block
The try-catch block is our go-to approach when it comes to quick exception handling. Letās see a code snippet that demonstrates the same.
Here is a basic implementation that we are all used to, yeah? Assume, the method GetData()
is a service call that is also prone to exceptions due to certain external factors. The thrown exception is caught by the catch block whose responsibility is to log the error to the console and return a status code of 500 Internal Server Error in this scenario.
For Logging, itās recommended to use Serilog
. As part of the .NET Series, we have already covered in-depth structured logging in ASP.NET Core using Serilog. Read here for more.
Letās say that there was an exception during the execution of the Get()
method. The below code is the exception that gets triggered.
Here is what you would be seeing on Swagger.
The Console may get you a bit more details on the exception, like the line number and other trace logs.
Although this is a simple way of handling exceptions in ASP.NET Core applications, this can also increase the lines of code of our application. Yes, you could have this approach for very simple and small POC applications. Imagine having to write the try-catch block in every controllerās action and other service methods. Pretty repetitive and not feasible, yeah?
It would be ideal if there was a way to handle all the exceptions centrally in one location, right? In the next sections, we will see 2 such approaches that can drastically improve our exception-handling mechanism by isolating all the handling logic to a single area. This not only gives a better codebase but also a more controlled application with even lesser exception handling concerns.
Default Exception Handling Middleware in .NET - UseExceptionHandler
To make things easier, UseExceptionHandler
Middleware comes out of the box with ASP.NET Core applications. This when configured in the Program.cs
, adds a middleware to the pipeline of the application that will catch any exceptions in and out of the application. A very straightforward implementation of middleware.
Letās see how UseExceptionHandler
can be configured. Open up the Program.cs
class of your ASP.NET Core application and configure the following.
This is a very basic setup & usage of UseExceptionHandler
Middleware in your ASP.NET Core applications. So, whenever there is an exception that is detected within the Pipeline of the application, the control falls back to this middleware, which in return will send a custom response to the request sender.
In this case, a status code of 400 Bad Request is sent along with the Message content of the original exception which in our scenario is āAn error occurredā¦ā. Pretty straightforward, yeah? Here is how the exception is displayed on Swagger.
Now, whenever there is an exception thrown in any part of the application, this middleware catches it and throws the required exception back to the client. Much cleaned-up code, yeah? But there are still more ways to make this better, by miles.
Custom Exceptions
Itās important and cleaner to segregate your error types. This lets you, at a later point in time to decide how your application should react to specific types of errors. Letās create Custom Exception classes that can essentially make your application throw more sensible exceptions that can be easily understood.
Create a new folder named Exceptions and add a new class named BaseException
. Make sure that you inherit Exception as the base class. Here is what the custom exception looks like.
So, this will be the base exception class, inheriting which, other exception classes can be created. This is a far cleaner approach while designing your exception classes. For example, you can create a new class named ProductNotFoundException
which inherits from this BaseException
class.
For example,
So, in any of your product-related service classes, if the product is not found within your database/cache, you can simply throw the ProductNotFoundException
and pass the ID of the product. And anyone that goes through the error logs would have instant clarity on what the error is, and for which product the error has occurred, instead of going through trace logs. As simple as that.
Here is how you would be using this Custom Exception class that we created now.
Get the idea, right? In this way, you can differentiate between exceptions. To get even more clarity related to this scenario, letās say we have other custom exceptions like StockExpiredException
, CustomerInvalidException
, and so on. Just give some meaningful names so that you can easily identify them. Now you can use these exception classes wherever the specific exception arises. This sends the related exception to the middleware, which has logic to handle it.
Custom Middleware - Global Exception Handling In ASP.NET Core [Old Method]
Now that we have our custom exception classes ready, letās create a Custom Global Exception Handling Middleware that gives even more control to the developer and makes the error-handling process much better.
Custom Global Exception Handling Middleware - Firstly, what is it? Itās a piece of code that can be configured as a middleware in the ASP.NET Core pipeline which contains our custom error handling logic. There are a variety of exceptions that can be caught by this pipeline.
Now, letās create the Global Exception Handling Middleware. Create a new class and name it ErrorHandlerMiddleware
- Line #5 has a simple try-catch block over the request delegate. It means that whenever there is an exception of any type in the pipeline for the current request, the control goes to the catch block. In this middleware, the Catch block has the error-handling logic.
- Line #9 catches all the Exceptions. Remember, all our custom exceptions are derived from the Exception base class.
- Lines #13 to #17 have a neat switch expression that can allow us to set the status code of the returned response based on the exception type. This is where custom exception classes can come in handy.
- Line #15 fetches the status code of the custom exception of type
BaseException
and sets it to the status code of the response. For instance, whenever aProductNotFound
exception is thrown, the status code we had set earlier in ourProductNotFoundException
class, which is 404 will be fetched here. This how helpful the custom exception classes are! - In lines #18 to #22, we create a new
ProblemDetails
class where we will fill in error-related information like status code, message, and other custom properties if needed. - Line #25 - Finally, the created problems detail model is serialized and sent as a response.
Before running this implementation, make sure that you donāt miss adding this middleware to the application pipeline. Open up the Program.cs
class and add the following line.
Make sure that you comment out or delete the UseExceptionHandler
default middleware as it may cause unwanted clashes. It doesnāt make sense to have multiple middlewares doing the same thing, yeah?
Additionally, I have added a Minimal endpoint with the route as ā/ā, which directly throws the ProductNotFoundException
exception.
With that done, letās run the application and see how the error gets displayed on Swagger.
There you go! You can see how well-built the response is and how easy it is to read what the API has to say to the client. Now, we have a completely custom-built error-handling mechanism, all in one place. And yes, of course as mentioned earlier, you are always free to add more properties to the ProblemDetails class that suits your applicationās needs.
IExceptionHandler in .NET 8 and above [Recommended]
IExceptionHandler
is an interface that was introduced as part of .NET 8, and is the recommended approach while handling exceptions globally. This interface is internally used by ASP.NET Core applications for their built-in exception-handling mechanism as well. This is an improved approach considering the other mechanisms that we have gone through.
The IExceptionHandler
interface wants you to implement a single method, TryHandleAsync
which works with the HTTP context and the actual error object. Another advantage is that you wonāt have to write an additional middleware for this to work, since it uses the already available UseExceptionHandler
middleware of .NET, which we have seen earlier.
This way, you will be able to separately define the error handling mechanism for every error, if needed. This helps build a more modular and maintainable code base.
Letās see IExceptionHandler
in action.
The TryHandleAsync
is where all the exception-handling logic resides. As you see in line #19, this method should always return true
if the exception is handled as required. Else, if the exception is not handled, or for any of your use cases, it can return false. This is typically applicable when you want to chain multiple such IExceptionHandler
implementations for multiple errors. We will learn about this in the next section!
Once the IExceptionHandler
implementation is completed, navigate to Program.cs
and add in the following.
The above ensures that your IExceptionHandler
implementation is registered into the service container of the application along with ProblemDetails. Also, as we saw earlier, you will need to add the built-in exception middleware to the pipeline.
Simply run the application, and invoke the minimal API endpoint that we had created earlier.
As you can see, we can see an almost similar response to earlier.
Silencing Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware Logs
If you see the console output, you will notice additional log messages that come directly from the built-in middleware. I often tend to silence this error message because I want to log the error message directly from my handlers. To silence the message from Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware
source, simply open up the appsettings.json
and add the following line to the Logging
section. I have set the log level to None
. This ensures that we wonāt be seeing the middleware logs anymore.
Handling Multiple Errors with IExceptionHandler
Earlier we discussed a scenario where we would want to write separate handler classes for each exception. To elaborate on this point, letās say you have 2 custom exception classes that you want to handle separately, ProductNotFoundException
and StockExhaustedException
.
In this case, you would want to define 2 handler classes that inherit from IExceptionHandler
.
Here, as soon as the control falls to TryHandleAsync
, it first checks if the type of exception is ProductNotFoundException
. If the exception type does not match, it simply returns a false, which means that the exception is not handled. In this case, the next exception handler chained into the pipeline will come into play.
As I said, you can chain your exception handlers this way, where the ProductNotFoundExceptionHandler
will be executed first. If the error is not handled, the next chained handler, which is the StockExhaustedExceptionHandler
will try to handle the error. This way you can build a well decoupled and modular application with ease.
Thatās it for today. I hope you all had an interesting read!
Summary
In this article, we have looked through various ways to implement Exception handling in our ASP.NET Core applications. The recommended approach for any .NET 8 or above applications would be to use the IExceptionHandler
interface since it provides more control and readability within your application code. Have any suggestions or questions? Feel free to leave them in the comment section below. Thanks and Happy Coding! š