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+json
responses. - 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.TraceIdentifier
asrequestId
andActivity.Current?.Id
astraceId
. - Middleware order matters:
UseExceptionHandler()
should be early so downstream exceptions are caught.