.NET 8 Series has started! Join Now for FREE

9 min read

Global Exception Handling in ASP.NET Core - Ultimate Guide

#dotnet

In this article, we will learn about Global Exception Handling in ASP.NET Core applications. Exceptions are something inevitable in any application however well the codebase is. This can usually occur due to external factors as well, like network issues and so on. If these exceptions are not handled well within the application, it may even lead the entire application to terminations and data loss.

The source code for the implementation can be found here.

Getting started with Exception Handling in ASP.NET Core

For this demonstration, We will be working on a new ASP.NET Core Web API Project. I will be using Visual Studio 2019 as my default IDE.

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.

[HttpGet]
public IActionResult Get()
{
try
{
var data = GetData(); //Assume you get some data here which is also likely to throw an exception in certain cases.
return Ok(data);
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return StatusCode(500);
}
}

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 returns a status code of 500 Internal Server Error in this scenario.

To learn more about logging in ASP.NET Core Applications, I recommend you to go through the following articles that demonstrate Logging with probably the best 2 Logging Frameworks for ASP.NET Core - Serilog & NLog

Letā€™s say that there was an exception during the execution of the Get() method. The below code is the exception that gets triggered.

throw new Exception("An error occurred...");

Here is what you would be seeing on Swagger.

global-exception-handling-in-aspnet-core

The Console may get you a bit more details on the exception, like the line number and other trace logs.

global-exception-handling-in-aspnet-core

Although this is a simple way for handling exceptions, this can also increase the lines of code of our application. Yes, you could have this approach for very simple and small applications. Imagine having to write the try-catch block in each and 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 logics to a single area. This not only gives a better codebase but a more controlled application with even lesser exception handling concerns.

Default Exception Handling Middleware in ASP.NET Core

To make things easier, UseExceptionHandler Middleware comes out of the box with ASP.NET Core applications. This when configured in the Configure method of the startup class adds a middleware to the pipeline of the application that will catch any exceptions in and out of the application. Thatā€™s how Middlewares and pipelines work, yeah?

Letā€™s see how UseExceptionHandler is implemented. Open up the Configure method in the Startup class of your ASP.NET Core application and configure the following.

app.UseExceptionHandler(
options =>
{
options.Run(async context =>
{
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
context.Response.ContentType = "text/html";
var exceptionObject = context.Features.Get<IExceptionHandlerFeature>();
if (null != exceptionObject)
{
var errorMessage = $"{exceptionObject.Error.Message}";
await context.Response.WriteAsync(errorMessage).ConfigureAwait(false);
}});
}
);

This is a very basic setup & usage of UseExceptionHandler Middleware. 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 straight-forward, yeah? Here is how the exception is displayed on Swagger.

global-exception-handling-in-aspnet-core

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 consumer. Much cleaned-up code, yeah? But there are still more ways to make this better, by miles.

Custom Middleware - Global Exception Handling In ASP.NET Core

In this section letā€™s create a Custom Global Exception Handling Middleware that gives even more control to the developer and makes the entire 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 logics. There are a variety of exceptions that can be caught by this pipeline.

We will also be creating Custom Exception classes that can essentially make your application throw more sensible exceptions that can be easily understood.

But before that, letā€™s build a Response class that I recommend to be a part of every project you build, at least the concept. So, the idea is to make your ASP.NET Core API send uniform responses no matter what kind of requests it gets hit with. This make the work easier for whoever is consuming your API. Additionally it gives a much experience while developing.

Create a new class ApiResponse and copy down the following.

public class ApiResponse<T>
{
public T Data { get; set; }
public bool Succeeded { get; set; }
public string Message { get; set; }
public static ApiResponse<T> Fail(string errorMessage)
{
return new ApiResponse<T> { Succeeded = false, Message = errorMessage };
}
public static ApiResponse<T> Success(T data)
{
return new ApiResponse<T> { Succeeded = true, Data = data };
}
}

The ApiResponse class is of a generic type, meaning any kind of data can be passed along with it. Data property will hold the actual data returned from the server. Message contains any Exceptions or Info message in string type. And finally there is a boolean that denotes if the request is a success. You can add multiple other properties as well depending on your requirement.

We also have Fail and Success method that is built specifically for our Exception handling scenario. You can find how this is being used in the upcoming sections.

As mentioned earlier, letā€™s also create a custom exception. Create a new class and name it SomeException.cs or anything. Make sure that you inherit Exception as the base class. Here is how the custom exception looks like.

public class SomeException : Exception
{
public SomeException() : base()
{
}
public SomeException(string message) : base(message)
{
}
public SomeException(string message, params object[] args) : base(String.Format(CultureInfo.CurrentCulture, message, args))
{
}
}

Here is how you would be using this Custom Exception class that we created now.

throw new SomeException("An error occurred...");

Get the idea, right? In this way you can actually differentiate between exceptions. To get even more clarity related to this scenario, letā€™s say we have other custom exceptions like ProductNotFoundException , StockExpiredException, CustomerInvalidException and so on. Just give some meaningful names so that you can easily identify. Now you can use these exception classes wherever the specific exception arises. This sends the related exception to the middleware, which has logics to handle it.

Now, letā€™s create the Global Exception Handling Middleware. Create a new class and name it ErrorHandlerMiddleware.cs

public class ErrorHandlerMiddleware
{
private readonly RequestDelegate _next;
public ErrorHandlerMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception error)
{
var response = context.Response;
response.ContentType = "application/json";
var responseModel = ApiResponse<string>.Fail(error.Message);
switch (error)
{
case SomeException e:
// custom application error
response.StatusCode = (int)HttpStatusCode.BadRequest;
break;
case KeyNotFoundException e:
// not found error
response.StatusCode = (int)HttpStatusCode.NotFound;
break;
default:
// unhandled error
response.StatusCode = (int)HttpStatusCode.InternalServerError;
break;
}
var result = JsonSerializer.Serialize(responseModel);
await response.WriteAsync(result);
}
}
}

Line 3 - RequestDelegate denotes a HTTP Request completion.
Line 10 - 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, control goes to the catch block. In this middleware, Catch block has all the goodness.

Line 14 - Catches all the Exceptions. Remember, all our custom exceptions are derived from the Exception base class.
Line 18 - Creates an APIReponse Model out of the error message using the Fail method that we created earlier.
Line 21 - In case the caught exception is of type SomeException, the status code is set to BadRequest. You get the idea, yeah? The other exceptions are also handled in a similar fashion.
Line 34 - Finally, the created api-response model is serialized and send as a response.

Before running this implementation, make sure that you donā€™t miss adding this middleware to the application pipeline. Open up the Startup.cs / Configure method and add in the following line.

app.UseMiddleware<ErrorHandlerMiddleware>();

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?

I also assume that you have done the necessary changes that will throw the SomeException Exception in the Get method of the default controller you are working with.

With that done, letā€™s run the application and see how the error getā€™s displayed on Swagger.

global-exception-handling-in-aspnet-core

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 API Reponses class that suits your applicationā€™s needs.

I have been using this approach for literally all of my open source projects, and itā€™s With that, letā€™s wrap up the article for now ;)

Summary

In this article, we have looked through various ways to implement Exception handling in our ASP.NET Core applications. The favorite approach should definitely be the one where we implemented Global Exception Handling in ASP.NET Core using Custom Middlewares. You can also find theĀ complete source code on my Github here. Have any suggestions or questions? Feel free to leave them in the comments section below. Thanks and Happy Coding! šŸ˜€

Source Code āœŒļø
Grab the source code of the entire implementation by clicking here. Do Follow me on GitHub .
Support ā¤ļø
If you have enjoyed my content and code, do support me by buying a couple of coffees. This will enable me to dedicate more time to research and create new content. Cheers!
Share this Article
Share this article with your network to help others!
What's your Feedback?
Do let me know your thoughts around this article.

Boost your .NET Skills

I am starting a .NET 8 Zero to Hero Series soon! Join the waitlist.

Join Now

No spam ever, we are care about the protection of your data. Read our Privacy Policy