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: