Roadmap › Exception Handling & Custom Error Handling in .NET — Every Way, Every App Type › Module: Error Handling by Application Type
Lessons in this course (9)
6. ASP.NET Core — every way to handle exceptions
Intermediate★ Must knowLesson 6 of 9
1. Developer exception page (development only — shows stack traces):
csharp
if (app.Environment.IsDevelopment()) app.UseDeveloperExceptionPage(); // on by default in Development with WebApplication2. Exception handler middleware + IExceptionHandler (.NET 8+) — the recommended global handler:
csharp
builder.Services.AddProblemDetails(o =>
o.CustomizeProblemDetails = ctx => ctx.ProblemDetails.Extensions["traceId"] = Activity.Current?.Id ?? ctx.HttpContext.TraceIdentifier);
builder.Services.AddExceptionHandler<AppExceptionHandler>(); // can register several; they run in order until one returns true
var app = builder.Build();
app.UseExceptionHandler(); // MVC apps: app.UseExceptionHandler("/Home/Error") for an HTML page
app.UseStatusCodePages(); // ProblemDetails/HTML bodies for empty 4xx/5xx responses (e.g. 404)
public sealed class AppExceptionHandler(ILogger<AppExceptionHandler> logger, IProblemDetailsService problems) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(HttpContext http, Exception ex, CancellationToken ct)
{
var (status, title) = ex switch
{
NotFoundException => (StatusCodes.Status404NotFound, "Resource not found"),
BusinessRuleException => (StatusCodes.Status422UnprocessableEntity, "Business rule violated"),
ConflictException or DbUpdateConcurrencyException => (StatusCodes.Status409Conflict, "Conflict"),
ValidationException => (StatusCodes.Status400BadRequest, "Validation failed"),
OperationCanceledException => (499, "Client closed request"),
_ => (StatusCodes.Status500InternalServerError, "An unexpected error occurred")
};
if (status >= 500) logger.LogError(ex, "Unhandled exception for {Method} {Path}", http.Request.Method, http.Request.Path);
else logger.LogWarning("Handled {Exception}: {Message}", ex.GetType().Name, ex.Message);
http.Response.StatusCode = status;
return await problems.TryWriteAsync(new ProblemDetailsContext
{
HttpContext = http,
Exception = ex,
ProblemDetails = new ProblemDetails
{
Status = status,
Title = title,
Detail = status < 500 ? ex.Message : null, // never leak internals for 500s
Extensions = { ["errorCode"] = (ex as AppException)?.ErrorCode }
}
});
}
}3. Custom middleware (works on every .NET Core version; same idea):
csharp
public sealed class ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger)
{
public async Task InvokeAsync(HttpContext context)
{
try { await next(context); }
catch (Exception ex)
{
logger.LogError(ex, "Unhandled exception");
if (context.Response.HasStarted) throw; // can't change a response already being sent
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new { title = "Unexpected error", traceId = context.TraceIdentifier });
}
}
}
app.UseMiddleware<ErrorHandlingMiddleware>(); // register FIRST so it wraps everything4. MVC exception filters — only for exceptions thrown inside controllers/actions/filters (not middleware, not routing):
csharp
public sealed class DomainExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext ctx)
{
if (ctx.Exception is BusinessRuleException br)
{
ctx.Result = new UnprocessableEntityObjectResult(new ProblemDetails { Title = br.Message, Status = 422 });
ctx.ExceptionHandled = true;
}
}
}
builder.Services.AddControllers(o => o.Filters.Add<DomainExceptionFilter>());5. Validation errors — [ApiController] automatically returns 400 ValidationProblemDetails when model binding/validation fails; customise with ConfigureApiBehaviorOptions(o => o.InvalidModelStateResponseFactory = ...).
6. Status code pages for non-exception errors — UseStatusCodePagesWithReExecute("/Error/{0}") renders a friendly page for 404/403 in MVC apps.
| Mechanism | Catches | Best for |
|---|---|---|
UseExceptionHandler + IExceptionHandler | Everything after it in the pipeline | Global, recommended |
| Custom middleware | Everything after it | Older versions, special needs |
| Exception filters | Exceptions in MVC actions/filters | Mapping domain exceptions per controller |
try/catch in action | That action | Expected, local recovery |
UseStatusCodePages | Empty error status responses | Consistent 404/401/403 bodies |
| Developer exception page | Everything (dev) | Debugging only |