Global Error Handling in ASP.NET Core 8

Milan Jovanović - Dec 4 '23 - - Dev Community

Exceptions are for exceptional situations. I even wrote about avoiding exceptions entirely.

But they will inevitably happen in your applications, and you need to handle them.

You can implement a global exception handling mechanism or handle only specific exceptions.

ASP.NET Core gives you a few options on how to implement this. So which one should you choose?

Today, I want to show you an old and new way to handle exceptions in ASP.NET Core 8.

Old Way: Exception Handling Midleware

The standard to implement exception handling in ASP.NET Core is using middleware. Middleware allows you to introduce logic before or after executing HTTP requests. You can easily extend this to implement exception handling. Add a try-catch statement in the middleware and return an error HTTP response.

There are 3 ways to create middleware in ASP.NET Core:

The convention-based approach requires you to define an InvokeAsync method.

Here's an ExceptionHandlingMiddleware defined by convention:

public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionHandlingMiddleware> _logger;

    public ExceptionHandlingMiddleware(
        RequestDelegate next,
        ILogger<ExceptionHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception exception)
        {
            _logger.LogError(
                exception, "Exception occurred: {Message}", exception.Message);

            var problemDetails = new ProblemDetails
            {
                Status = StatusCodes.Status500InternalServerError,
                Title = "Server Error"
            };

            context.Response.StatusCode =
                StatusCodes.Status500InternalServerError;

            await context.Response.WriteAsJsonAsync(problemDetails);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The ExceptionHandlingMiddleware will catch any unhandled exception and return a Problem Details response. You can decide how much information you want to return to the caller. In this example, I'm hiding the exception details.

You also need to add this middleware to the ASP.NET Core request pipeline:

app.UseMiddleware<ExceptionHandlingMiddleware>();
Enter fullscreen mode Exit fullscreen mode

New Way: IExceptionHandler

ASP.NET Core 8introduces a new IExceptionHandlerabstraction for managing exceptions. The built-in exception handler middleware uses IExceptionHandler implementations to handle exceptions.

This interface has only one TryHandleAsync method.

TryHandleAsync attempts to handle the specified exception within the ASP.NET Core pipeline. If the exception can be handled, it should return true. If the exception can't be handled, it should return false. This allows you to implement custom exception-handling logic for different scenarios.

Here's a GlobalExceptionHandler implementation:

internal sealed class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;

    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
    {
        _logger = logger;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        _logger.LogError(
            exception, "Exception occurred: {Message}", exception.Message);

        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status500InternalServerError,
            Title = "Server error"
        };

        httpContext.Response.StatusCode = problemDetails.Status.Value;

        await httpContext.Response
            .WriteAsJsonAsync(problemDetails, cancellationToken);

        return true;
    }
}


internal sealed class GlobalExceptionHandler : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        // Handle the exception, log errors.

        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

Configuring IExceptionHandler Implementations

You need two things to add an IExceptionHandler implementation to the ASP.NET Core request pipeline:

  1. Register the IExceptionHandler service with dependency injection
  2. Register the ExceptionHandlerMiddlewarewith the request pipeline

You call the AddExceptionHandler method to register the GlobalExceptionHandler as a service. It's registered with a singleton lifetime. So be careful about injecting services with a different lifetime.

I'm also calling AddProblemDetails to generate a Problem Details response for common exceptions.

builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
Enter fullscreen mode Exit fullscreen mode

You also need to call UseExceptionHandler to add the ExceptionHandlerMiddleware to the request pipeline:

app.UseExceptionHandler();
Enter fullscreen mode Exit fullscreen mode

Chaining Exception Handlers

You can add multiple IExceptionHandler implementations, and they're called in the order they are registered. A possible use case for this is using exceptions for flow control.

You can define custom exceptions like BadRequestException and NotFoundException. They correspond with the HTTP status code you would return from the API.

Here's a BadRequestExceptionHandler implementation:

internal sealed class BadRequestExceptionHandler : IExceptionHandler
{
    private readonly ILogger<BadRequestExceptionHandler> _logger;

    public GlobalExceptionHandler(ILogger<BadRequestExceptionHandler> logger)
    {
        _logger = logger;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        if (exception is not BadRequestException badRequestException)
        {
            return false;
        }

        _logger.LogError(
            badRequestException,
            "Exception occurred: {Message}",
            badRequestException.Message);

        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status400BadRequest,
            Title = "Bad Request",
            Detail = badRequestException.Message
        };

        httpContext.Response.StatusCode = problemDetails.Status.Value;

        await httpContext.Response
            .WriteAsJsonAsync(problemDetails, cancellationToken);

        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

And here's a NotFoundExceptionHandler implementation:

internal sealed class NotFoundExceptionHandler : IExceptionHandler
{
    private readonly ILogger<NotFoundExceptionHandler> _logger;

    public GlobalExceptionHandler(ILogger<NotFoundExceptionHandler> logger)
    {
        _logger = logger;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        if (exception is not NotFoundException notFoundException)
        {
            return false;
        }

        _logger.LogError(
            notFoundException,
            "Exception occurred: {Message}",
            notFoundException.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;
    }
}
Enter fullscreen mode Exit fullscreen mode

You also need to register both exception handlers by calling AddExceptionHandler:

builder.Services.AddExceptionHandler<BadRequestExceptionHandler>();
builder.Services.AddExceptionHandler<NotFoundExceptionHandler>();
Enter fullscreen mode Exit fullscreen mode

The BadRequestExceptionHandler will execute first and try to handle the exception. If the exception isn't handled, NotFoundExceptionHandler will execute next and attempt to handle the exception.

Takeaway

Using middleware for exception handling is an excellent solution in ASP.NET Core. However, it's great that we have new options using the IExceptionHandler interface. I will use the new approach in ASP.NET Core 8 projects.

I'm very much against using exceptions for flow control. Exceptions are a last resort when you can't continue normal application execution. The Result pattern is a better alternative.

Exceptions are also extremely expensive, as David Fowler noted:

Image description

If you want to get rid of exceptions in your code, check out this video.

Thanks for reading, and stay awesome!


P.S. Whenever you're ready, there are 2 ways I can help you:

  1. Pragmatic Clean Architecture: This comprehensive course will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture. Join 1,600+ students here.

  2. Patreon Community: Think like a senior software engineer with access to the source code I use in my YouTube videos and exclusive discounts for my courses. Join 930+ engineers here.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .