ASP.NET Core cookbook · Intermediate
Map exceptions to ProblemDetails with IExceptionHandler
Unhandled exceptions leak stack traces, and domain errors all become 500s.
Approach
Register AddProblemDetails() and an IExceptionHandler that maps domain exceptions to status codes, and let
the default handler produce a generic 500 for everything else. Add the trace ID to every problem response.
Solution
using System.Diagnostics;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails(options =>
options.CustomizeProblemDetails = ctx =>
ctx.ProblemDetails.Extensions["traceId"] = Activity.Current?.Id ?? ctx.HttpContext.TraceIdentifier);
builder.Services.AddExceptionHandler<DomainExceptionHandler>();
var app = builder.Build();
app.UseExceptionHandler();
app.UseStatusCodePages();
app.MapGet("/orders/{id}", (string id) => id == "o1" ? Results.Ok() : throw new NotFoundException($"Order {id} not found"));
app.Run();
public class NotFoundException(string message) : Exception(message);
public class ConflictException(string message) : Exception(message);
public class DomainExceptionHandler(IProblemDetailsService problems, ILogger<DomainExceptionHandler> logger) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(HttpContext context, Exception exception, CancellationToken ct)
{
int? status = exception switch
{
NotFoundException => StatusCodes.Status404NotFound,
ConflictException => StatusCodes.Status409Conflict,
_ => null,
};
if (status is null)
{
logger.LogError(exception, "Unhandled exception on {Path}", context.Request.Path);
return false; // default handler writes a generic 500
}
context.Response.StatusCode = status.Value;
return await problems.TryWriteAsync(new ProblemDetailsContext
{
HttpContext = context,
Exception = exception,
ProblemDetails = new ProblemDetails { Status = status, Title = exception.Message },
});
}
}
Pitfalls
UseDeveloperExceptionPagein production.- Exceptions for expected flow in hot paths; consider result types for very frequent "not found" cases.
- Logging the same exception in the handler and again in middleware.
Variations
- Register several handlers; they run in registration order until one returns
true. - Return
TypedResults.Problem(...)directly from handlers for expected errors.
Further reading: Handle errors in APIs
Reference: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling-api
More ASP.NET Core recipes
- Accept file uploads with size and type limits
- Add OpenTelemetry traces and metrics
- Apply EF Core migrations safely during deployment
- Bind and validate options at startup
- Cache responses with output caching and evict by tag
- Call external APIs with typed HttpClients and resilience
- Configure CORS for a browser front end
- Expose liveness and readiness health checks