
.NET Global Exception Handler to Return Problem Details For Your APIs
May 14, 2025 By Esau Silva
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.