Create an exception handler in ASP.NET Core 8

Take advantage of the new IExceptionHandler interface to handle exceptions gracefully in your ASP.NET Core applications.

error neon mistake
Florent Darrault (CC BY-SA 2.0)

Microsoft’s November release of .NET 8 brought all kinds of great new features. One of the nice improvements introduced in ASP.NET Core 8 is IExceptionHandler, an interface that makes it easier to handle exceptions gracefully in ASP.NET Core web applications.

Error handling has a long history in programming languages and frameworks. IExceptionHandler simplifies error handling by providing a callback and a central location for handling known exceptions. In this article we’ll discuss how you can use IExceptionHandler in your ASP.NET Core 8 applications and present meaningful error responses to the user.

To use the code examples provided in this article, you should have Visual Studio 2022 installed in your system. If you don’t already have a copy, you can download Visual Studio 2022 here.

Create an ASP.NET Core Web API project in Visual Studio 2022

To create an ASP.NET Core 8 Web API project in Visual Studio 2022, follow the steps outlined below.

  1. Launch the Visual Studio 2022 IDE.
  2. Click on “Create new project.”
  3. In the “Create new project” window, select “ASP.NET Core Web API” from the list of templates displayed.
  4. Click Next.
  5. In the “Configure your new project” window, specify the name and location for the new project.
  6. Optionally check the “Place solution and project in the same directory” check box, depending on your preferences.
  7. Click Next.
  8. In the “Additional Information” window shown next, select “.NET 8.0 (Long Term Support)” as the framework version, and ensure that the “Use controllers” box is unchecked. We’ll be using minimal APIs in this project.
  9. Elsewhere in the “Additional Information” window, leave the “Authentication Type” set to “None” (the default) and make sure the check boxes “Enable Open API Support,” “Configure for HTTPS,” and “Enable Docker” remain unchecked. We won’t be using any of those features here.
  10. Click Create.

We’ll use this ASP.NET Core Web API project to work with the IExceptionHandler interface in the sections below.

Why do we need an exception handler?

In ASP.NET Core, an exception handler is a component that can handle exceptions globally in an application. It can catch all unhandled exceptions and then generate appropriate error responses.

An exception handler can help implement a centralized error-handling mechanism, allowing your applications to fail gracefully. This will enable you to ensure that all exceptions are handled, errors are correctly logged and processed, and meaningful error responses are generated and presented to the user.

Let us understand this with an example. Consider the following code.

using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
using System.Net;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseExceptionHandler(opt => { });
app.MapGet("/GenerateError", () =>
{
    throw new NotImplementedException();
});
app.Run();

When you execute the application and hit the /GenerateError endpoint, the response displayed in your web browser will appear as shown below.

iexceptionhandler 01 IDG

Figure 1: Response generated without using exception handlers.

Note that the error response is not formatted and it is quite difficult to read and understand the error metadata from this response.

Introducing the IExceptionHandler interface

Exception handling has been used in programming languages for decades to handle run-time errors in applications. ASP.NET Core 8 improves exception handling significantly with the introduction of the IExceptionHandler interface. You can create a central class for exception handling in ASP.NET Core 8 by implementing the IExceptionHandler interface.

The IExceptionHandler interface looks like this:

public interface IExceptionHandler
{
    ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken);
}

The IExceptionHandler interface contains the declaration of the TryHandleAsync method. This method accepts three parameters: an instance of type HttpContext, an Exception, and a CancellationToken. It returns ValueTask<bool> to indicate if the exception was handled successfully or not.

When you implement the TryHandleAsync method in your class that extends the IExceptionHandler interface, you must return a boolean value from this method. You should return true if the exception was handled or false otherwise.

Create a custom exception handler in ASP.NET Core

To implement the IExceptionHandler interface, create a new class named GlobalExceptionHandler in a file of the same name with a .cs extension. And enter the following code in there.

public class GlobalExceptionHandler(IHostEnvironment hostEnvironment, ILogger<GlobalExceptionHandler> logger)
    : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
    {
        return true;
    }
}

Your custom error handling logic should reside in the TryHandleAsync method. The following code snippet shows how you can handle exceptions that occur in your application asynchronously.

private const string ExceptionMessage = "An unhandled exception has occurred while executing the request.";

public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)

{

    logger.LogError(exception, exception is Exception ? exception.Message : ExceptionMessage);

    var problemDetails = CreateProblemDetails(httpContext, exception);

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

    return true;

}

We’ll now take advantage of the open source ProblemDetails middleware to generate consistent, structured, machine-readable error messages. The CreateProblemDetails method returns an instance of ProblemDetails containing the error metadata, as shown in the code snippet given below.

private ProblemDetails CreateProblemDetails(in HttpContext httpContext, in Exception exception)

{

    httpContext.Response.ContentType = "application/json";

    switch (exception)

    {

        case NotImplementedException notImplementedException:

            httpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;               

            break;

        default:

            httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;               

            break;

    }

    return new ProblemDetails

    {

        Status = (int)httpContext.Response.StatusCode,

        Type = exception.GetType().Name,

        Title = "An unexpected error occurred",

        Detail = exception.Message,

        Instance = $"{httpContext.Request.Method} {httpContext.Request.Path}"

    };

}

Complete source code of our custom exception handler

The following code listing comprises the complete source code of the GlobalExceptionHandler class.

public class GlobalExceptionHandler(IHostEnvironment hostEnvironment, ILogger<GlobalExceptionHandler> logger)

    : IExceptionHandler

{

    private const string ExceptionMessage = "An unhandled exception has occurred while executing the request.";

    public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)

    {

        logger.LogError(exception, exception is Exception ? exception.Message : ExceptionMessage);

        var problemDetails = CreateProblemDetails(httpContext, exception);

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

        return true;

    }

    private ProblemDetails CreateProblemDetails(in HttpContext httpContext, in Exception exception)

    {

        httpContext.Response.ContentType = "application/json";

        switch (exception)

        {

            case NotImplementedException notImplementedException:

                httpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;               

                break;

            default:

                httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;               

                break;

        }

        return new ProblemDetails

        {

            Status = (int)httpContext.Response.StatusCode,

            Type = exception.GetType().Name,

            Title = "An unexpected error occurred",

            Detail = exception.Message,

            Instance = $"{httpContext.Request.Method} {httpContext.Request.Path}"

        };

    }

}

Register the exception handler with the request processing pipeline

Now that our custom exception handler is ready, we need to register it with the request processing pipeline to start using it in our application. You can register the exception handler by including the following line of code in the Program.cs file.

builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

Let us now create an error handling endpoint in Program.cs. When invoked this endpoint will display a user-friendly error response as specified in the configured error handler. To create an error handling endpoint, enter the following code in the Program.cs file.

app.MapGet("/GenerateError", () =>

{

    throw new ValidationException();

});

The call to the UseExceptionHandler() method configures the request processing pipeline in ASP.NET Core to handle exceptions using our middleware.

app.UseExceptionHandler(opt => { });

Now when you execute the application and hit the /GenerateError endpoint, the response displayed in the web browser will appear as below. Much better!

iexceptionhandler 02 IDG

Figure 2: Error response generated using an error handler.

It is always advisable to implement your own custom exception handling in your application. This ensures not only that the exception handling logic meets your requirements, but also that the error responses are generated in your desired format. Naturally, rolling your own exception handler also gives you much more granular control over the exception handling mechanism.

Copyright © 2024 IDG Communications, Inc.