# Global Error Handling in ASP.NET Core 8

> Exceptions are for exceptional situations, but they will inevitably happen and you need to handle them. ASP.NET Core gives you a few options here, so which one should you choose? I want to show you an old and a new way to handle exceptions in ASP.NET Core 8.

Published: 2023-12-02. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/global-error-handling-in-aspnetcore-8

ASP.NET Core 8 gives you two ways to handle exceptions globally.
The old way is custom middleware that wraps the request in a `try-catch` and returns a `ProblemDetails` response.
The new way is the `IExceptionHandler` abstraction, registered with `AddExceptionHandler` and plugged into the pipeline with `UseExceptionHandler`.

Exceptions are for exceptional situations.
I even wrote about [**avoiding exceptions entirely.**](https://milanjovanovic.tech/blog/functional-error-handling-in-dotnet-with-the-result-pattern)

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**](https://milanjovanovic.tech/blog/3-ways-to-create-middleware-in-asp-net-core) in ASP.NET Core:

- Using [**request delegates**](https://milanjovanovic.tech/blog/3-ways-to-create-middleware-in-asp-net-core#adding-middleware-with-request-delegates)
- By [**convention**](https://milanjovanovic.tech/blog/3-ways-to-create-middleware-in-asp-net-core#adding-middleware-by-convention)
- [`IMiddleware`](https://milanjovanovic.tech/blog/3-ways-to-create-middleware-in-asp-net-core#adding-factory-based-middleware)

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

Here's an `ExceptionHandlingMiddleware` defined by convention:

```csharp
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);
        }
    }
}
```

The `ExceptionHandlingMiddleware` will catch any unhandled exception and return a [Problem Details](https://www.rfc-editor.org/rfc/rfc7807.html) 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:

```csharp
app.UseMiddleware<ExceptionHandlingMiddleware>();
```

## New Way: IExceptionHandler

[ASP.NET Core 8](https://learn.microsoft.com/en-us/aspnet/core/introduction-to-aspnet-core?view=aspnetcore-8.0)
introduces a new [`IExceptionHandler`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandler?view=aspnetcore-8.0)
abstraction 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:

```csharp
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;
    }
}
```

## 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 [`ExceptionHandlerMiddleware`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.exceptionhandlermiddleware?view=aspnetcore-8.0)
   with the request pipeline

You call the `AddExceptionHandler` method to register the `GlobalExceptionHandler` as a service.
It's registered with a [**singleton lifetime**](https://milanjovanovic.tech/blog/improving-aspnetcore-dependency-injection-with-scrutor).
So be careful about injecting services with a different lifetime.

I'm also calling `AddProblemDetails` to generate a [Problem Details](https://www.rfc-editor.org/rfc/rfc7807.html) response for common exceptions.

```csharp
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
```

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

```csharp
app.UseExceptionHandler();
```

## 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**](https://milanjovanovic.tech/blog/rest-api-http-status-codes) you would return from the API.

Here's a `BadRequestExceptionHandler` implementation:

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

    public BadRequestExceptionHandler(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;
    }
}
```

And here's a `NotFoundExceptionHandler` implementation:

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

    public NotFoundExceptionHandler(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;
    }
}
```

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

```csharp
builder.Services.AddExceptionHandler<BadRequestExceptionHandler>();
builder.Services.AddExceptionHandler<NotFoundExceptionHandler>();
```

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**](https://milanjovanovic.tech/blog/functional-error-handling-in-dotnet-with-the-result-pattern) is a better alternative.

Exceptions are also [extremely expensive](https://github.com/dotnet/aspnetcore/issues/46280#issuecomment-1527898867),
as David Fowler noted:

![David Fowler explaining that exceptions are extremely expensive in the ASP.NET Core pipeline](https://milanjovanovic.tech/blogs/mnw_066/fowler_comment.png)

If you want to get rid of exceptions in your code, [**check out this video.**](https://youtu.be/WCCkEe_Hy2Y)

Thanks for reading, and stay awesome!

---

## Frequently asked questions

### How do you handle exceptions globally in ASP.NET Core?

There are two main options: write a custom exception handling middleware that wraps the request in a try-catch and returns an error response, or use the IExceptionHandler abstraction that ASP.NET Core 8 added, which plugs into the built-in exception handler middleware.

### What is IExceptionHandler in ASP.NET Core?

ASP.NET Core 8 introduced IExceptionHandler, an abstraction for managing exceptions. It has a single TryHandleAsync method that returns true when it handles the exception and false to let another handler try, which enables custom handling logic per scenario.

### How do you register an IExceptionHandler in ASP.NET Core?

Call builder.Services.AddExceptionHandler with your handler type and AddProblemDetails, then add app.UseExceptionHandler to put the ExceptionHandlerMiddleware in the pipeline. Handlers are registered with a singleton lifetime, so be careful injecting services with a different lifetime.

### Can you chain multiple IExceptionHandler implementations?

Yes. Handlers run in registration order. When TryHandleAsync returns false, the next handler attempts to handle the exception. For example, a BadRequestExceptionHandler can map custom exceptions to 400 responses and a NotFoundExceptionHandler to 404 responses.

### Should you use exceptions for flow control in .NET?

No. Exceptions are a last resort for when you cannot continue normal execution, and they are extremely expensive in the ASP.NET Core pipeline. The Result pattern is a better alternative for expected failure cases.
