Validation and error handling

ASP.NET Core · Practical · 12 min

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.

Check yourself

  1. What does UseExceptionHandler with AddProblemDetails return for an unhandled exception in Production?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling