Robust APIs fail predictably. In .NET 8 you can standardize failures with
Problem Details (RFC 7807, obsoleted by RFC 9457) and centralize error handling with the
exception handler middleware and IExceptionHandler.
- Returns RFC-compliant error payloads.
- Surfaces FluentValidation failures as
400 Bad Request. - Distinguishes unexpected failures as
500 Internal Server Error. - Enriches responses with
requestId,traceId, timestamp, path, method, user id, and field-levelerrors. - Keeps controllers/endpoints clean—no per-action try/catch.
Problem Details (RFC 7807 → RFC 9457)
Problem Details is a machine-readable error contract for HTTP APIs. The object contains
type, title, status, detail, and instance.
You can also add extensions—custom fields that your clients can rely on (e.g., traceId, errors).
RFC 9457 is the current spec and obsoletes RFC 7807.
RFC 9457 ·
RFC 7807
What Microsoft provides in .NET 8
- AddProblemDetails() registers Problem Details so built-in middleware can emit
application/problem+jsonresponses. - IExceptionHandler +
AddExceptionHandler<T>(): register your global exception handler implementation. - UseExceptionHandler(): activate the middleware early in the pipeline so unhandled exceptions are converted to Problem Details.
The shape of validation responses
For FluentValidation failures, return 400 with field-level messages. Reference 400 semantics per RFC 7231 §6.5.1 in type and add diagnostics in extensions:
{
"type": "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1",
"title": "Validation error",
"status": 400,
"detail": "One or more validation errors occurred.",
"instance": "/api/notes",
"traceId": "0HNEHBMOKNNJ9:00000004",
"requestId": "0HNEHBMOKNNJ9:00000004",
"timestamp": "2025-08-02T00:44:17.4974375Z",
"method": "POST",
"path": "/api/notes",
"userId": "5cc67c3f-9890-4f69-ab38-00a87941caf3",
"errors": {
"CreateNoteDto.Language": [
"Language code must be exactly 2 characters.",
"Language code must be a valid ISO 639-1 code (e.g., 'en', 'es', 'fr')."
]
},
"errorCount": 1,
"category": "ValidationFailure"
}
Step-by-step implementation
1) Add FluentValidation
dotnet add package FluentValidation.AspNetCore
Register it and suppress the automatic model-state 400 so the global handler can control the response shape:
// Program.cs
builder.Services
.AddControllers()
.AddFluentValidation(cfg => cfg.RegisterValidatorsFromAssemblyContaining<Program>());
builder.Services.Configure<ApiBehaviorOptions>(o => o.SuppressModelStateInvalidFilter = true);
builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
2) Write a validator and throw ValidationException in your endpoint
// DTO
public sealed record CreateNoteDto(string Title, string Language, string Content);
// Validator
public sealed class CreateNoteDtoValidator : AbstractValidator<CreateNoteDto>
{
public CreateNoteDtoValidator()
{
RuleFor(x => x.Title).NotEmpty().MaximumLength(120);
RuleFor(x => x.Language)
.Length(2).WithMessage("Language code must be exactly 2 characters.")
.Must(code => new[] { "en","es","fr","uk","ru" }.Contains(code))
.WithMessage("Language code must be a valid ISO 639-1 code (e.g., 'en', 'es', 'fr').");
RuleFor(x => x.Content).NotEmpty();
}
}
3) Implement a global exception handler (IExceptionHandler)
public sealed class GlobalExceptionHandler : IExceptionHandler
{
// ... see repository for full code
public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken ct)
{
if (exception is ValidationException vex) { /* build 400 problem */ }
// else: build 500 problem
}
}
4) Wire it up in Program.cs
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseExceptionHandler(); // <-- activate handler early
app.MapControllers();
app.Run();
Distinguish internal server errors
Unexpected faults should map to 500 with a generic title and sanitized detail. Link to RFC 7231 §6.6.1 in type:
RFC 7231 §6.6.1 — 500 Internal Server Error
Best practices
- Log server errors in the handler with
ILogger. Include method, path, and trace id; avoid PII. - Use Problem Details everywhere: AddProblemDetails() wires writers used by exception and status code middleware.
- Keep RFC semantics: 400 → RFC 7231 §6.5.1; 500 → §6.6.1.
- Correlation IDs: include
HttpContext.TraceIdentifierasrequestIdandActivity.Current?.IdastraceId. - Middleware order matters:
UseExceptionHandler()should be early so downstream exceptions are caught.