Handling Exceptions with IExceptionHandler in ASP.NET Core 8

In this post, I’ll describe three ways to handle custom exceptions in ASP.NET Core 8


For my examples, I use Minimal APIs.

Let’s create a simple endpoint for getting a user by ID that throws NotFoundException.

app.MapGet("/user/{id}", (int id) =>
{
    throw new NotFoundException(id, "User");
});

The implementation of NotFoundException is following.

public class NotFoundException : Exception
{
    public NotFoundException(int id, string name) 
        : base($"{name} with id {id} not found.")
    {
    }
}
When we request our endpoint, you'll get an Internal Server Error with a stack trace.

We don’t want to return our stack trace because it can expose sensitive information about our server and potentially compromise its security.

Problem Details

We can use the AddProblemDetails service to create a ProblemDetails object for failed requests. The ProblemDetails class is a machine-readable format for specifying errors in HTTP API responses based on RFC standards.

builder.Services.AddProblemDetails();

Let’s execute our endpoint again and see the response.

The response changed, but we still see the stack trace in the exception property. You can customize problem details. Let’s do that.

builder.Services.AddProblemDetails(opt =>
    opt.CustomizeProblemDetails = context => context.ProblemDetails
        = new ProblemDetails 
        { 
            Detail = context.ProblemDetails.Detail,
            Status = context.ProblemDetails.Status,
            Title = context.ProblemDetails.Title,
            Type = context.ProblemDetails.Type
        });

The response is the following.

Yeah! We have standardized responses according to RFC standards. We hid the stack trace.

The first issue is that the customization of ProblemDetails could be more handy. The second issue is that we get an Internal Server Error but want to get a Not Found 404 HTTP code.

We can handle our custom exceptions using ProblemDetails.

builder.Services.AddProblemDetails(opt =>
    opt.CustomizeProblemDetails = context =>
        {
            if (context.Exception is NotFoundException notFoundException)
            {
                context.ProblemDetails = new ProblemDetails 
                {
                    Detail = notFoundException.Message,
                    Status = StatusCodes.Status404NotFound,
                    Title = "Resource not found",
                    Type = context.ProblemDetails.Type
                };
            }

            context.HttpContext.Response.StatusCode 
                = context.ProblemDetails.Status.Value;
        });

The response is the following.

Finally, we handled our custom exception!

You can notice that the syntax is not very readable. Of course, you can move the implementation to a separate method or class.

There is a little bit better solution.

Middleware

The different way to handle custom exceptions in ASP.NET Core is middleware.

Middleware refers to components that handle requests and responses as they move through an ASP.NET Core application’s pipeline. The middleware components are arranged in a pipeline, each performing a specific function in processing the HTTP request or response. Middleware components in ASP.NET Core can perform tasks such as authentication, authorization, logging, routing, and more.

So, let’s use the middleware to handle our exceptions. First, we need to create a middleware. There are a few ways to do that. You can build it using delegates, conventions, or a middleware factory. Read more in “3 Methods to Create Middleware in ASP.NET Core“.

I prefer a middleware factory. Therefore, I implement an IMiddleware interface.

public class ExceptionHandlingMiddleware : IMiddleware
{ 
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        try
        {
            await next(context);
        }
        catch (NotFoundException ex)
        {
            var problemDetails = new ProblemDetails
            {
                Detail = ex.Message,
                Status = StatusCodes.Status404NotFound,
                Title = "Resource not found",
                Type = ex.GetType().Name
            };

            context.Response.StatusCode = problemDetails.Status.Value;

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

We need to register the created middleware in the DI and use it in our application.

builder.Services.AddTransient<ExceptionHandlingMiddleware>();

app.UseMiddleware<ExceptionHandlingMiddleware>();

The response is the following.

The implementation is the same as that of ProblemDetails customization. However, the code is more readable, as it’s in a separate class by design, and it’s easier to debug.

However, we have to catch our exception. In the real application, there will be many custom exceptions. The implementation of catching different exceptions will grow. We can create many middlewares each to catch specific exceptions. But everywhere we should catch it.

Let’s see what new ASP.NET Core 8 brings to us.

IExceptionHandler

ASP.NET Core 8 introduces IExceptionHandler. It represents an interface for handling exceptions.

Please don’t confuse it with IExceptionFilter in ASP.NET Core MVC! You can use it only in MVC apps. The IExceptionHandler belongs to ASP.NET Core.

Let’s create our exception handler. 

public class NotFoundExceptionHandler : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext context, 
        Exception exception, 
        CancellationToken cancellationToken)
    {
        if (exception is NotFoundException)
        {
            var problemDetails = new ProblemDetails
            {
                Detail = exception.Message,
                Status = StatusCodes.Status404NotFound,
                Title = "Resource not found",
                Type = exception.GetType().Name
            };

            context.Response.StatusCode = problemDetails.Status.Value;

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

            return true;
        }

        return false;
    }
}

The implementation is similar, but we don’t need to catch the exception. We only need to check the type of our exception.

As middlewares, we need to register our exception handler and use it.

builder.Services.AddExceptionHandler<NotFoundExceptionHandler>();

app.UseExceptionHandler(_ => { });

If you don’t have any configuration for the exception handler, you can get rid of the ugly _ => {} by registering ProblemDetails as it was shown in the section with ProblemDetails.

The difference you can spot is that the return value is bool. If we return true, the exception’s processing stops. If we return false, the pipeline will continue processing its execution and invoke the next Exception handler.

It means you can chain many exception handlers. They process exceptions one by one until one of them returns true. So the registration order matters! If you have the global exception handler for unhandled exceptions, it must be registered as the last one.

builder.Services.AddExceptionHandler<NotFoundExceptionHandler>();
builder.Services.AddExceptionHandler<BadRequestExceptionHandler>();
builder.Services.AddExceptionHandler<UnhandledExceptionHandler>();

Summary

In this post, I described three ways to handle exceptions in the ASP.NET Core.

The IExceptionHandler is a brand-new way to do that in ASP.NET Core 8.

However, it’s not all ways to handle exceptions in ASP.NET Core. You can read more about that in the Microsoft Documentation.

Scroll to Top