Global Error Handling in ASP.NET Core 8
Tags: aspnetcore, dotnet, csharp, errors
Published: 2023-12-04 12:44:36

---
title: Global Error Handling in ASP.NET Core 8
published: true
date: 2023-12-02 00:00:00 UTC
tags: aspnetcore,dotnet,csharp,errorhandling
canonical_url: https://www.milanjovanovic.tech/blog/global-error-handling-in-aspnetcore-8
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0u022terhonn3xqx6gdk.png
---
Exceptions are for exceptional situations. I even wrote about [**avoiding exceptions entirely.**](https://www.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://www.milanjovanovic.tech/blog/3-ways-to-create-middleware-in-asp-net-core) in ASP.NET Core:

- Using [**request delegates**](https://www.milanjovanovic.tech/blog/3-ways-to-create-middleware-in-asp-net-core#adding-middleware-with-request-delegates)
- By [**convention**](https://www.milanjovanovic.tech/blog/3-ways-to-create-middleware-in-asp-net-core#adding-middleware-by-convention)
- [`IMiddleware`](https://www.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;
    }
}


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

        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://www.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 you would return from the API.

Here's a `BadRequestExceptionHandler` implementation:

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

And here's a `NotFoundExceptionHandler` implementation:

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

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://www.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:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l8rcpzrp6l1jccshfrgu.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!

* * *

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

1. [**Pragmatic Clean Architecture:**](https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&utm_medium=website&utm_campaign=cross-posting) 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.**](https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&utm_medium=website&utm_campaign=cross-posting)

2. [**Patreon Community:**](https://www.patreon.com/milanjovanovic) 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.**](https://www.patreon.com/milanjovanovic)