Enhanced Exception Handling with IExceptionHandler in .NET Core 8

Introduction

Appropriate error handling is essential to determining how our applications behave. It entails creating uniform response formats for our applications so that, even in the event of errors, we can maintain a standard procedure. The IExceptionHandler abstraction included with.NET 8, gives us a new and better method for handling errors in our applications.

What is an IExceptionHandler?

An interface called IExceptionHandler is used in ASP.NET Core applications to handle exceptions. It outlines an interface that we can use to deal with various exceptions. This enables us to create unique logic that responds to specific exceptions or sets of exceptions according to their type, logging and generating customized error messages and responses.

Why use IExceptionHandler?

We can create more reliable and user-friendly APIs by utilizing IExceptionHandler, which provides a strong and adaptable method for handling exceptions in .NET APIs. In order to handle various exception types, we can also implement IExceptionHandler in standard C# classes. By doing this, we can easily maintain and modularize our applications.

By customizing the responses to the individual exceptions, IExceptionHandler enables us to provide more detailed error messages. This is useful for creating user interfaces that allow us to route users to a specific error page in the event that one of our APIs throws an exception.

Furthermore, because .NET 8 already has the middleware implemented for us through the IExceptionHandler interface, we don't always need to create custom global exception handlers for our applications when using the IExceptionHandler interface.

As an illustration

Let’s create the GlobalExceptionHandler.cs file in our application.

public class GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger = logger;
    public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
    {
        _logger.LogError(exception, "An unexpected error occurred");
        string? currentId = Activity.Current?.Id ?? httpContext.TraceIdentifier;
        Dictionary<string, object> otherDetails = new()
        {
            { "CurrentId", currentId },
            { "TraceId", Convert.ToString(Activity.Current!.Context.TraceId)! }
        };
        await httpContext.Response.WriteAsJsonAsync(
            new ProblemDetails
            {
                Status = (int)HttpStatusCode.InternalServerError,
                Type = exception.GetType().Name,
                Title = "An unexpected error occurred",
                Detail = exception.Message,
                Instance = $"{httpContext.Request.Method} {httpContext.Request.Path}",
                Extensions = otherDetails!
            },
            cancellationToken
        );
        return true;
    }
}

Next, let's set up the dependency injection container to register our exception handler.

builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

Our application will automatically call the GlobalExceptionHandler handler whenever an exception occurs. According to RFC 7807 specifications, the API will also produce ProblemDetails standardized responses. We will have more control over how to handle, format, and intercept error responses in this way.

Let's now complete the application request pipeline by adding the middleware:

app.UseExceptionHandler(opt => { });

Let’s run the application and execute the API so we are able to see whether our global exception handler is executing when any exception is raised in our application and showing the details in the API response or not.

API Response

ILogger

Server response

Managing various Exception Types

We implement distinct instances of IExceptionHandler, each handling a particular type of exception when handling various error types. Next, by invoking the AddExceptionHandler<THandler>() extension method, we can register each handler in the dependency injection container. It's important to remember that we should register exception handlers in the order in which we want them to be executed.

Let’s create an exception handler to handle The timeout exception handler.

public class TimeoutExceptionHandler(ILogger<TimeoutExceptionHandler> logger) : IExceptionHandler
{
    private readonly ILogger<TimeoutExceptionHandler> _logger = logger;

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext, 
        Exception exception, 
        CancellationToken cancellationToken)
    {
        _logger.LogError(exception, "A timeout occurred");
        if (exception is not TimeoutException)
        {
            return false;
        }
        httpContext.Response.StatusCode = (int)HttpStatusCode.RequestTimeout;
        string? currentId = Activity.Current?.Id ?? httpContext.TraceIdentifier;
        Dictionary<string, object> otherDetails = new()
        {
            { "CurrentId", currentId },
            { "TraceId", Convert.ToString(Activity.Current!.Context.TraceId)! }
        };
        await httpContext.Response.WriteAsJsonAsync(
            new ProblemDetails
            {
                Status = (int)HttpStatusCode.RequestTimeout,
                Type = exception.GetType().Name,
                Title = "A timeout occurred",
                Detail = exception.Message,
                Instance = $"{httpContext.Request.Method} {httpContext.Request.Path}",
                Extensions = otherDetails
            }, 
            cancellationToken);
        return true;
    }
}

Let’s register the services in the dependency injection.

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

Public class

Request URL

We learned the new technique and evolved together.

Happy coding!