In the world of API development, consistency is king. One of the most common areas where consistency breaks down is error handling. Different endpoints might return different JSON structures for a 404 Not Found versus a 500 Internal Server Error, forcing API consumers to write complex, brittle parsing logic.
ASP.NET Core provides a clean, built-in solution to this problem: the ProblemDetails middleware. By adopting this standard, you can ensure all your API’s error responses have a predictable, machine-readable shape.
What is Problem Details?
ProblemDetails is a specification defined in RFC 7807 that establishes a standard format for returning error information from an HTTP API. It’s essentially a contract for what an error response should look like.
A standard ProblemDetails object includes these key fields:
- type: A URI that identifies the problem type. This can link to documentation explaining the error.
- title: A short, human-readable summary of the problem.
- status: The HTTP status code generated by the server for this occurrence of the problem.
- detail: A human-readable explanation specific to this occurrence of the problem.
- instance: A URI that identifies the specific occurrence of the problem.
Here’s an example of what a ProblemDetails response looks like in JSON:
{
"type": "[https://tools.ietf.org/html/rfc7231#section-6.5.4](https://tools.ietf.org/html/rfc7231#section-6.5.4)",
"title": "Not Found",
"status": 404,
"detail": "The requested resource with ID '123' was not found.",
"instance": "/api/products/123"
}
This standardized structure makes life much easier for client developers, who can now build a single, robust mechanism for handling all API errors.
Enabling the ProblemDetails Middleware
Integrating ProblemDetails into your ASP.NET Core application is straightforward. It involves registering the services and adding the appropriate middleware to your request pipeline.
In your Program.cs file, make the following additions:
var builder = WebApplication.CreateBuilder(args);
// 1. Register the ProblemDetails service
builder.Services.AddProblemDetails();
var app = builder.Build();
// 2. Add the Exception Handler Middleware
// This must be one of the first middleware components added.
app.UseExceptionHandler();
// For non-exception errors (like 404), this helps produce ProblemDetails.
app.UseStatusCodePages();
app.MapGet("/products/{id}", (int id) =>
{
if (id == 0)
{
throw new ArgumentOutOfRangeException(nameof(id), "Product ID cannot be zero.");
}
// Pretend we can't find the product
return Results.NotFound($"Product with ID {id} not found.");
});
app.Run();
With just these few lines, your application is now configured to automatically handle errors and exceptions by generating ProblemDetails responses.
AddProblemDetails(): Registers the necessary services, including the defaultIProblemDetailsService.UseExceptionHandler(): This is the key middleware that catches unhandled exceptions thrown anywhere in the pipeline and uses theIProblemDetailsServiceto generate a response.UseStatusCodePages(): This middleware intercepts responses that have an error status code (4xx-5xx) but no body, and generates aProblemDetailsbody for them.
If you run the application and request /products/0, the ArgumentOutOfRangeException will be caught, and you’ll receive a 500 Internal Server Error with a ProblemDetails body. If you request /products/123, the Results.NotFound() will trigger the UseStatusCodePages middleware to generate a 404 response with a ProblemDetails body.
Customizing ProblemDetails Responses
The default implementation is great, but you’ll often want to add more context to your error responses, like a correlation or trace ID. ASP.NET Core provides flexible options for customization.
Adding Custom Properties Globally
You can easily add custom data to all ProblemDetails responses by configuring the service during registration. A common use case is adding the current TraceIdentifier, which is invaluable for debugging and correlating logs.
Modify the AddProblemDetails call in Program.cs:
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = context =>
{
// Add a trace identifier to all problem details responses
var traceId = System.Diagnostics.Activity.Current?.Id
?? context.HttpContext.TraceIdentifier;
context.ProblemDetails.Extensions.Add("traceId", traceId);
};
});
Now, every error response from your API will include a traceId field, linking it directly to server-side logs.
Callout: A Win for API Consumers
From my experience building and consuming APIs, a consistent error shape is a massive quality-of-life improvement. When clients know they can always expect a
ProblemDetailsobject, they can create generic error handling components in their frontend or client applications. This eliminates guesswork and reduces the amount of boilerplate code needed to handle API failures gracefully.
Handling Specific Exceptions
For more granular control, you can map specific exception types to custom ProblemDetails responses. This is perfect for converting domain-specific exceptions into well-defined HTTP errors.
First, let’s define a custom exception:
public class InvalidRequestDataException(string detail) : Exception(detail);
Next, configure the ExceptionHandlerMiddleware to handle this specific exception type. This approach uses the IProblemDetailsService directly to write a customized response.
// In Program.cs
app.UseExceptionHandler(exceptionHandlerApp =>
{
exceptionHandlerApp.Run(async context =>
{
var exceptionHandlerFeature = context.Features.Get<Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature>();
var exception = exceptionHandlerFeature?.Error;
var problemDetailsService = context.RequestServices.GetService<Microsoft.AspNetCore.Diagnostics.IProblemDetailsService>();
if (exception is InvalidRequestDataException invalidRequestEx)
{
var problemDetails = new Microsoft.AspNetCore.Mvc.ProblemDetails
{
Status = StatusCodes.Status400BadRequest,
Title = "Invalid Request Data",
Detail = invalidRequestEx.Message,
Instance = context.Request.Path
};
// Set a custom property
problemDetails.Extensions["errorCode"] = "VAL-100";
context.Response.StatusCode = problemDetails.Status.Value;
await context.Response.WriteAsJsonAsync(problemDetails);
}
else if (problemDetailsService is not null)
{
// Fallback to the default IProblemDetailsService for all other exceptions
await problemDetailsService.TryWriteAsync(new() { HttpContext = context, Exception = exception });
}
});
});
app.MapGet("/submit", () =>
{
// Example endpoint that throws our custom exception
throw new InvalidRequestDataException("The 'name' field cannot be empty.");
});
In this more advanced setup:
- We define a custom
UseExceptionHandlerlambda. - We check if the caught exception is our
InvalidRequestDataException. - If it is, we create a custom
ProblemDetailsobject, setting the status to 400 and adding a customerrorCodeextension. - For any other exception, we fall back to the default
IProblemDetailsServiceto ensure consistent handling.
Conclusion
The ProblemDetails middleware in ASP.NET Core is a powerful tool for standardizing API error handling. By moving away from ad-hoc error objects to the RFC 7807 standard, you create APIs that are more robust, predictable, and easier for client developers to consume. The setup is minimal, the customization options are flexible, and the benefits to your API’s overall design and usability are significant.