Effective error handling plays a crucial role in shaping the behavior of our applications. It involves establishing consistent response formats for our applications, ensuring a standardized approach even when errors occur. .NET 8 comes with the IExceptionHandler abstraction which offers us a new improved approach to handling errors in our applications.

In this article, we’re going to learn how we can use IExceptionHandler to handle exceptions in .NET.

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

What Is IExceptionHandler in .NET

IExceptionHandler is an interface for handling exceptions in ASP.NET Core applications. It defines an interface that we can implement to handle different exceptions. This allows us to write custom logic for handling individual exceptions or groups of exceptions based on their type, in turn providing tailored responses, error messages as well as logging.

Why Use IExceptionHandler

IExceptionHandler offers a powerful and flexible approach to handling exceptions in .NET APIs, empowering us to build more robust and user-friendly APIs. Also, we can implement IExceptionHandler in normal C# classes to handle different types of exceptions. This way, we make our applications modular and easy to maintain.

IExceptionHandler helps us tailor the responses to the specific exceptions, providing more informative error messages. This helps when building user interfaces such that we can redirect users to a dedicated error page whenever exceptions are thrown by our APIs.

In addition to that, when using the IExceptionHandler interface, we don’t necessarily need to create custom global exception handlers for our applications because .NET 8 already has the middleware implemented for us through the IExceptioHandler.

To learn more about global exception handlers, be sure to check out our article Global Error Handling in ASP.NET Core Web API.

Let’s see how we can use IExceptionHandler in our applications.

Project Setup

Since IExceptionHandler ships with .NET 8, we can use it out of the box. First, let’s create a Web API project using the dotnet CLI command:

dotnet new web

For tests, we’ll simulate a library service. So let’s create a Book class:

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Author { get; set; }
}

Let’s also create an ILibraryService interface with two methods:

public interface ILibraryService
{
    Book GetById(int id);
    List<Book> GetAllBooks();
}

Next, let’s add an endpoint to test our implementation:

app.MapGet("/books", async context =>
{
    var libraryService = context
    .RequestServices
    .GetRequiredService<ILibraryService>();

    var allBooks = libraryService.GetAllBooks();
    await context.Response.WriteAsJsonAsync(allBooks);
});

Now, we can run our application and test the /books endpoint to get a list of the books:

[
  {
    "id": 1,
    "title": "The Catcher in the Rye",
    "author": "J.D. Salinger"
  },
  {
    "id": 2,
    "title": "To Kill a Mockingbird",
    "author": "Harper Lee"
  },
  {
    "id": 3,
    "title": "1984",
    "author": "George Orwell"
  }
]

This implies that our setup works as expected, let’s dig deeper into exception handling.

Using the IExceptionHandler Middleware in our Applications

Having set up our project, let’s now add the GlobalExceptionHandler class which implements IExceptionHandler:

public class GlobalExceptionHandler: IExceptionHandler
{
}

Then, let’s register our exception handler in the dependency injection container:

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

Whenever there’s an exception in our application, the GlobalExceptionHandler handler will be automatically invoked. The API will also generate ProblemDetails standardized responses as per RFC 7807 specification. This way, we have more control over how we intercept, handle, and format error responses.

Finally, let’s add the middleware to the application request pipeline:

app.UseExceptionHandler();

Now, let’s proceed to handle exceptions.

The TryHandleAsync Method

The IExceptionHandler interface only has the TryHandleAsync() method that enables us to implement custom exception handling logic for different exceptions. The method tries to handle the specified exception asynchronously within the ASP.NET Core pipeline. It returns a task that represents the asynchronous read operation. The Result property of the task contains the result of the exception-handling operation. If the exception is handled successfully, the method returns true, otherwise it returns false.

Let’s add a custom implementation of TryHandleAsync() method in our middleware:

public async ValueTask<bool> TryHandleAsync(
    HttpContext httpContext,
    Exception exception,
    CancellationToken cancellationToken)
{
    logger.LogError(
        $"An error occurred while processing your request: {exception.Message}");

    var problemDetails = new ProblemDetails
    {
        Status = (int)HttpStatusCode.InternalServerError,
        Type = exception.GetType().Name,
        Title = "An unhandled error occurred",
        Detail = exception.Message
    };

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

    return true;
}

First, we log the exception, then use the ProblemDetails object to populate additional details of the exception. Please note that we have the flexibility to create a custom class for shaping the response, eliminating the need to rely exclusively on ProblemDetails.

Let’s add an endpoint in the Program class to test this:

app.MapGet("/books/get-by-author", () =>
{
    throw new NotImplementedException();
});

Calling the endpoint, the application throws a NotImplementedException exception. 

Let’s test this out:

{
  "type": "NotImplementedException",
  "title": "An unhandled error occurred",
  "status": 500,
  "detail": "The method or operation is not implemented."
}

Our exception handler catches the exception, handles it, and returns the expected response format.

Having learned how to set up and handle exceptions using IExceptionHandler, let’s see how we can handle different types of exceptions in our applications.

Handling Different Exception Types

When handling different types of errors, we implement different instances of IExceptionHandler, each handling a specific type of exception. Then, we can proceed to register each handler in the dependency injection container by calling the  AddExceptionHandler<THandler>() extension method. It’s worth noting that when registering multiple exception handlers, we should follow the order we’d like to execute them. 

Let’s create an exception handler to handle BadHttpRequestException exceptions:

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

    if (exception is not BadHttpRequestException badRequestException)
    {
        return false;
    }

    var errorResponse = new ErrorResponse
    {
        StatusCode = (int)HttpStatusCode.BadRequest,
        Title = "Bad Request",
        Message = badRequestException.Message
    };

    httpContext.Response.StatusCode = errorResponse.StatusCode;

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

    return true;
}

Here, the TryHandleAsync() method uses the errorResponse object to return the exception details from the API, unlike the first exception handler which we used ProblemDetails. Having implemented the handler, let’s register it as a service: 

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

This way, whenever we get a BadHttpRequestException, our new middleware BadRequestExceptionHandler will handle it. Any other exceptions will be handled by GlobalExceptionHandler.

Let’s add an endpoint to test this:

app.MapGet("/books/{id}", async context =>
{
    var id = int.Parse(context.Request.RouteValues["id"].ToString());
    var libraryService = context.RequestServices.GetRequiredService<ILibraryService>();
    var book = libraryService.GetById(id);

    if (book is null)
    {
        throw new BadHttpRequestException($"Book with Id {id} not found.", StatusCodes.Status400BadRequest);
    }

    await context.Response.WriteAsJsonAsync(book);
});

This endpoint gets a book by its id passed as a route parameter. If the book exists, we get a 200 success response with the data, otherwise, we get a BadHttpRequestException.

Now, calling the endpoint with an id value that exists:

GET https://localhost:7036/books/1

We get a response with the book details:

{
  "id": 1,
  "title": "The Catcher in the Rye",
  "author": "J.D. Salinger"
}

If we are to test with an id that doesn’t exist:

GET https://localhost:7036/books/0

We get a response with a 400 BadRequest status code:

{
  "title": "BadHttpRequestException",
  "statusCode": 400,
  "message": "Book with Id 0 not found."
}

We could chain more exception handlers to handle the different exceptions by using the same approach.

This far, there’s one potential problem. We could end up with a lot of classes in our applications all handling different exceptions. How about we improve this?

Let’s modify our GlobalExceptionHandler class:

public async ValueTask<bool> TryHandleAsync(
    HttpContext httpContext,
    Exception exception,
    CancellationToken cancellationToken)
{
    _logger.LogError(
        $"An error occurred while processing your request: {exception.Message}");

    var errorResponse = new ErrorResponse
    {
        Message = exception.Message
    };

    switch (exception)
    {
        case BadHttpRequestException:
            errorResponse.StatusCode = (int)HttpStatusCode.BadRequest;
            errorResponse.Title = exception.GetType().Name;
            break;

        default:
            errorResponse.StatusCode = (int)HttpStatusCode.InternalServerError;
            errorResponse.Title = "Internal Server Error";
            break;
    }

    httpContext.Response.StatusCode = errorResponse.StatusCode;

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

    return true;
}

Here, we add a switch statement which we can extend to handle other exception types. In case there’s no matching exception, the application throws the default InternalServerError exception. This way, we only register one exception handler in the dependency injection container instead of registering multiple handlers.

Conclusion

In this article, we’ve learned how to handle exceptions using the IExceptionHandler interface introduced in .NET 8. We’ve learned how we can set it up in our applications and handle different types of exceptions thrown by our applications. We’ve also learned why we should use it in our applications, one of the reasons being that it helps us centralize exception handling and that we can implement it from normal classes without much boilerplate.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!