Global Exception Handling

Problem to solve : Handling exceptions globally in an ASP.NET Core Application.

Handling exceptions effectively is vital for building robust and user-friendly applications. If exceptions are not handled well within the application, it may break the entire application or even lead to data loss.

To avoid unexpected exceptions, we need to handle them properly. In ASP.NET Core, we can handle exceptions globally using the IMiddleware interface but on ASP.NET Core 8, we have a new way to handle exceptions using the IExceptionHandler interface.

This new interface only has one method, TryHandleAsync, that receives the HttpContext and the Exception as parameters. This method should return a bool indicating if the exception was handled or not.

Let's start by implementing the interface IExceptionHandler:

internal sealed class GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        logger.LogError(exception, "A generic exception occurred: {Message}", exception.Message);
 
        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status500InternalServerError,
            Title = "Internal Server error"
        };
 
        httpContext.Response.StatusCode = problemDetails.Status.Value;
 
        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
 
        return true;
    }
}


Now we need to register the GlobalExceptionHandler in the Program.cs file:

builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
 
...
 
app.UseExceptionHandler();


Here we also add the ProblemDetails middleware to handle the response in a standard way.

If an unhanded exception occurs, the GlobalExceptionHandler will log the exception and return a ProblemDetails object with a 500 status code.

Let's assume we are creating an api for a Video Game store, and we have a minimal api that returns the details of a game by id. We already create a DatabaseDemo on the previous post Entity Framework with PostgreSQL.

If the game is not found we throw a custom exception VideoGameNotFoundException:

internal sealed class VideoGameNotFoundException : Exception
{
    public VideoGameNotFoundException(Guid id) : base($"VideoGame with id {id} not found.")
    {
    }
}


We don't want to handle this exception in the GlobalExceptionHandler, we want to create a specific handler for it.

internal sealed class VideoGameNotFoundExceptionHandler(ILogger<VideoGameNotFoundExceptionHandler> logger) : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        if (exception is not VideoGameNotFoundException notFoundException)
        {
            return false;
        }
 
        logger.LogError(notFoundException, "Videogame not found.", exception.Message);
 
        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status404NotFound,
            Title = "Not Found",
            Detail = notFoundException.Message
        };
 
        httpContext.Response.StatusCode = problemDetails.Status.Value;
 
        await httpContext.Response
            .WriteAsJsonAsync(problemDetails, cancellationToken);
 
        return true;
    }
}


As we can see we only handle the VideoGameNotFoundException exception, if the exception is not of this type we return false. We must also register the VideoGameNotFoundExceptionHandler in the Program.cs file:

builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddExceptionHandler<VideoGameNotFoundExceptionHandler>();


If we run the application and try to get a game that doesn't exist, we will see the response with the status code 500 and the message "Internal Server error". That's because the order of the handlers is important, the GlobalExceptionHandler is registered first, so it will handle the exception.

builder.Services.AddExceptionHandler<VideoGameNotFoundExceptionHandler>();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();


Now if we try to get a game that doesn't exist, we will see the response with the status code 404 and the message "Not found".

On the console output, we will see 2 log messages, one comes from our handler the other from the built-in middleware. To remove the built-in middleware log we can add the following configuration to the appsettings.json file:

{
  "Logging": {
    "LogLevel": {
      ...
      "Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware": "None"
    }
  }
}




Hope this helps on your journey. Happy coding! 🚀