.NET Global Exception Handler to Return Problem Details For Your APIs

This article will focus on handling custom exceptions for flow control on .NET with a global exception handler and return a problem details object.

Having said that. There are, for the most part, two schools of thought. One where it is OK to throw a (custom) exception to control flow. i.e. Throw a UserNotFoundException when a user is not found. The second is implementing the Result Pattern where you return a Success or an Error to control the flow. i.e. Result.UserNotFound.

Problem Details

Problem details is the standard way of returning error messages from a REST API to the client.

Say your API is validating for a required field. i.e. Name. If the name is not present in the API request, instead of only returning a 400 Bad Request HTTP Status Code, you return a problem details response detailing what went wrong with the request so clients can take action on the invalid request.

The members of the problem details response are as follows:

  • type – identifies the problem details type
  • title – summary of the problem details
  • status – HTTP Status code of the issue
  • errors – details of what went wrong with the request
{
    "type": "https://httpstatuses.com/400",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "errors": {
        "Name": [
            "'Name' must not be empty."
        ]
    }
}

Global Exception Handler

You add the global exception handler to Program.cs like so

var app = builder.Build();
app.UseExceptionHandler(GlobalExceptionHandler.Configure);

Then the implementation of the GlobalExceptionHandler static class

public static class GlobalExceptionHandler
{
    public static void Configure(IApplicationBuilder builder)
    {
        builder.Run(async context => 
        {
            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            // Declare the problem results
            IResult problemResult;
        
            // Switch statement to match the custom exceptions
            switch (exceptionHandlerPathFeature?.Error)
            {
                case UserAlreadyExistsException:
                {
                    var details = new ProblemDetails
                    {
                        Type = "https://httpstatuses.com/409",
                        Title = "User already exists.",
                        Status = StatusCodes.Status409Conflict,
                    };

                    problemResult = Results.Problem(details);
                    break;
                }
                
                // Other custom exceptions, say UnauthorizedException and return
                // a 401 Unauthorized problem details
                
                // This custom exception here contains validation errors from
                // Fluent Validation
                case ApiValidationException:
                {
                    // Casting the exception to ApiValidationException to get the
                    // `Errors` property and send it back to the client
                    var exp = (ApiValidationException)exceptionHandlerPathFeature!.Error;
                
                    problemResult = Results.ValidationProblem
                    (
                        exp.Errors,
                        type: "https://httpstatuses.com/400",
                        statusCode: StatusCodes.Status400BadRequest
                    );
                    break;
                }
                
                // If no custom exception is matched, return generic 500 Internal Server
                // error response
                default:
                {
                    var details = new ProblemDetails
                    {
                        Type = "https://httpstatuses.com/500",
                        Title = "An error occurred while processing your request.",
                        Status = StatusCodes.Status500InternalServerError
                    };
            
                    problemResult = Results.Problem(details);
                    break;
                }
            }

            await problemResult.ExecuteAsync(context);
        });
    }
}

Bonus! Minimal APIs and Fluent Validation

With Minimal APIs, you have to invoke Fluent Validation inside the endpoint like so

var validationResult = await validator.ValidateAsync(userSignup, cancellationToken);
if (validationResult.IsValid is false) 
    throw new ApiValidationException(validationResult.ToDictionary());

In the example above, I map the validation results to ApiValidationException custom exception. The global exception handler will handle this exception returning the appropriate error response with a 400 Bad Request status code

And the ApiValidationException implementation details

public class ApiValidationException : Exception
{
    public IDictionary<string,string[]> Errors { get; }
    
    public ApiValidationException(IDictionary<string,string[]> errors)
        : base()
    {
        Errors = errors;
    }
}

.NET 8

There is a new way of handling global exceptions in .NET 8. I will write about it at some point. Having said that, the code shown here will also work with .NET 8.


If you liked this reading, share it on your social media and you can follow me on Twitter or LinkedIn.


Consider giving back by getting me a coffee (or a couple) by clicking the following button:

Spread the love

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.