Clients need predictable error responses, and unhandled exceptions must never leak stack traces.
Validation. ASP.NET Core 10 adds built-in validation for minimal APIs: call builder.Services.AddValidation() and annotate request types with DataAnnotations ([Required], [Range], [StringLength], [EmailAddress], [RegularExpression]). Invalid input gets a 400 with a ValidationProblemDetails body listing errors per field, before your handler runs. Controllers with [ApiController] have done this for a long time. For complex rules, implement IValidatableObject or use a library such as FluentValidation.
Problem details (RFC 9457) is the standard JSON error shape: type, title, status, detail, instance, plus extensions. builder.Services.AddProblemDetails() makes the framework use it for error responses, and TypedResults.Problem(...) / ValidationProblem(...) let you return it yourself.
Exceptions. Register app.UseExceptionHandler() early in the pipeline so unhandled exceptions become a 500 problem response instead of a raw stack trace. In Development, the developer exception page shows details. To map specific exceptions to status codes, implement IExceptionHandler and register it with AddExceptionHandler<T>(); return true from TryHandleAsync when you've written the response.
app.UseStatusCodePages() adds bodies to empty 4xx/5xx responses (such as a 404 from routing).
Throwing exceptions for expected outcomes (not found, validation) is costly and hides control flow; return results for those and reserve exceptions for the unexpected.
Example
using System.ComponentModel.DataAnnotations;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddValidation(); // ASP.NET Core 10
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseExceptionHandler(); // unhandled -> 500 problem+json
app.UseStatusCodePages();
app.MapPost("/users", (NewUser user) => TypedResults.Created($"/users/{user.Email}", user));
app.MapGet("/boom", string () => throw new InvalidOperationException("secret detail"));
app.MapGet("/teapot", () => TypedResults.Problem("No coffee here", statusCode: 418));
app.Run();
// POST /users {"email":"x","age":3} -> 400
// {"title":"One or more validation errors occurred.","status":400,
// "errors":{"Email":["The Email field is not a valid e-mail address."],
// "Age":["The field Age must be between 18 and 130."]}}
// GET /boom -> 500 {"title":"An error occurred while processing your request.",...}
// GET /teapot -> 418 {"title":"I'm a teapot","status":418,"detail":"No coffee here"}
public record NewUser([Required, EmailAddress] string Email, [Range(18, 130)] int Age);Remember
- AddValidation + DataAnnotations validate minimal API input automatically (.NET 10)
- ProblemDetails is the standard error body; enable it with AddProblemDetails
- UseExceptionHandler turns unhandled exceptions into safe 500 responses
- Return results for expected failures; keep exceptions exceptional
Watch out for
- Returning exception messages or stack traces to clients in production.