Developer Blog

  • home
  • Developer Blog
  • .NET Global Exception Handler to Return Problem Details For Your APIs

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

Published: May 14, 2025

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.