Global Exception Handling & Middleware
15 questions found
What is the purpose of global exception handling middleware in an ASP.NET Core application?
Beginner
Global exception handling middleware catches unhandled exceptions thrown anywhere downstream in the pipeline (controllers, other middleware, business logic), preventing them from crashing the request or leaking a raw stack trace to clients, and instead converting them into a consistent, well-formed error response (like a standardized JSON error body with an appropriate HTTP status code) -- centralizing error-handling logic in one place instead of scattering try/catch blocks throughout every controller action.
app.UseExceptionHandler("/error"); // catches any unhandled exception from everything after this line
app.Map("/error", (HttpContext context) => {
return Results.Problem(title: "An error occurred", statusCode: 500);
});
Real-world example
Without global exception handling, a single unhandled null reference exception deep in business logic would return a raw ASP.NET Core error page (or worse, an unhandled crash) instead of the consistent JSON error format all API consumers expect.
Common follow-ups: How does UseExceptionHandler differ from the newer IExceptionHandler interface in .NET 8?;What information should and shouldn't be included in an error response for security reasons?
ASP.NET Core Middleware & Request Pipeline;RESTful Web APIs & Controllers
How does the IExceptionHandler interface (introduced in .NET 8) provide a more structured alternative to the UseExceptionHandler lambda approach?
Intermediate
IExceptionHandler defines a TryHandleAsync(HttpContext, Exception, CancellationToken) method that you implement as a proper, testable, DI-injectable class rather than an inline delegate -- multiple exception handlers can be registered and are tried in order until one returns true (indicating it handled the exception), enabling different handlers for different exception types (like a specific handler for validation exceptions and a fallback handler for everything else) in a clean, composable way.
public class ValidationExceptionHandler : IExceptionHandler {
public async ValueTask<bool> TryHandleAsync(HttpContext context, Exception exception, CancellationToken ct) {
if (exception is not ValidationException ve) return false; // not handled, try next handler
context.Response.StatusCode = 400;
await context.Response.WriteAsJsonAsync(new { errors = ve.Errors }, ct);
return true;
}
}
builder.Services.AddExceptionHandler<ValidationExceptionHandler>();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>(); // fallback
Real-world example
An API registers a specific ValidationExceptionHandler that formats validation errors distinctly, chained with a catch-all GlobalExceptionHandler for everything else, replacing what used to be a large if/else chain inside a single exception-handling lambda.
Common follow-ups: What determines the order multiple registered IExceptionHandlers are tried in?;How does this integrate with ProblemDetails for standardized error responses?
RESTful Web APIs & Controllers;.NET CLI
SDK & Project Structure (csproj)
What is ProblemDetails (RFC 7807/9457), and how does ASP.NET Core support it for standardized error responses?
Advanced
ProblemDetails is a standardized JSON format for HTTP API error responses, including fields like type, title, status, detail, and instance, providing a consistent, machine-and-human-readable error structure across an entire API (and interoperably across different APIs following the same standard). ASP.NET Core has built-in support via Results.Problem(), AddProblemDetails(), and automatic ProblemDetails generation for validation failures and unhandled exceptions when configured, reducing the need to hand-roll a custom error response shape.
builder.Services.AddProblemDetails(); // enables automatic ProblemDetails for errors
app.UseExceptionHandler(); // now produces RFC-compliant ProblemDetails responses automatically
// Manual usage:
return Results.Problem(title: "Insufficient stock", statusCode: 409, detail: "Only 2 units remaining");
Real-world example
A public API adopts ProblemDetails universally so that third-party integrators consuming errors from any endpoint (validation failures, business rule violations, unhandled exceptions) always see the same consistent, well-documented JSON error shape.
Common follow-ups: What are the standard vs extension members of the ProblemDetails format?;How do you add custom extension fields to a ProblemDetails response?
RESTful Web APIs & Controllers;API Versioning
How do you map different exception types to different HTTP status codes in a global exception handler?
Intermediate
Inside your exception handler (whether the older UseExceptionHandler lambda or the newer IExceptionHandler), inspect the caught exception's type (or a custom base exception hierarchy your application defines) and map it to the semantically correct HTTP status code -- e.g., a NotFoundException maps to 404, a ValidationException to 400, an UnauthorizedAccessException to 403 -- rather than defaulting every unhandled exception to a generic 500.
public class GlobalExceptionHandler : IExceptionHandler {
public async ValueTask<bool> TryHandleAsync(HttpContext context, Exception exception, CancellationToken ct) {
var (statusCode, title) = exception switch {
NotFoundException => (404, "Resource not found"),
ValidationException => (400, "Validation failed"),
UnauthorizedAccessException => (403, "Access denied"),
_ => (500, "An unexpected error occurred")
};
context.Response.StatusCode = statusCode;
await context.Response.WriteAsJsonAsync(new { title }, ct);
return true;
}
}
Real-world example
An API's global handler correctly returns 404 for a custom ProductNotFoundException and 400 for a custom InvalidOrderStateException, giving API consumers semantically accurate status codes instead of a blanket 500 for every kind of failure.
Common follow-ups: What custom exception hierarchy design makes this mapping cleanest to maintain?;Should business logic ever throw exceptions for expected failure cases, or use a Result pattern instead?
Exception Handling;RESTful Web APIs & Controllers
What information should be excluded from error responses in production to avoid leaking sensitive implementation details?
Advanced
Production error responses should never include raw exception messages, stack traces, internal file paths, database connection details, or any information revealing internal architecture -- since these can aid an attacker in understanding and exploiting the system. Instead, log the full exception details server-side (where only authorized personnel can see them) while returning a generic, safe message to the client, optionally including a correlation/trace ID that lets support staff look up the full details in logs without exposing them directly.
public async ValueTask<bool> TryHandleAsync(HttpContext context, Exception exception, CancellationToken ct) {
var traceId = context.TraceIdentifier;
_logger.LogError(exception, "Unhandled exception. TraceId: {TraceId}", traceId); // full details logged
await context.Response.WriteAsJsonAsync(new {
title = "An unexpected error occurred", // safe, generic message to client
traceId // lets support correlate with server-side logs without exposing internals
}, ct);
return true;
}
Real-world example
A penetration test flags an API that was returning full .NET stack traces (including internal file paths and a database connection string fragment) in error responses -- fixed by ensuring UseDeveloperExceptionPage is strictly Development-only and production uses the safe, generic handler.
Common follow-ups: How does a correlation/trace ID help support teams without exposing sensitive data?;What's the risk specifically of exposing stack traces regarding internal architecture?
Authentication & Authorization (Identity
JWT
OAuth);Logging
How do you handle exceptions specifically within minimal API endpoints, given they don't use the traditional MVC exception filter pipeline?
Intermediate
Minimal APIs don't support MVC-style [ExceptionFilter] attributes, instead relying on the same pipeline-level exception handling middleware (UseExceptionHandler / IExceptionHandler) that applies globally across the whole application including minimal API endpoints, or using endpoint filters (IEndpointFilter) to wrap specific endpoints with custom try/catch logic for endpoint-specific exception handling needs beyond the global handler.
app.MapGet("/products/{id}", (int id, IProductService service) => {
var product = service.GetById(id) ?? throw new NotFoundException($"Product {id} not found");
return Results.Ok(product);
});
// Relies on global UseExceptionHandler/IExceptionHandler to catch NotFoundException
// and convert it to a proper 404 response, since there's no MVC filter pipeline here
Real-world example
A minimal API project relies entirely on global exception handling middleware for consistent error responses across all its endpoints, since there's no per-action exception filter attribute mechanism available like there would be in a traditional MVC controller-based API.
Common follow-ups: How do endpoint filters provide finer-grained exception handling than the global handler?;What's the migration path for exception filter logic when moving from MVC controllers to minimal APIs?
RESTful Web APIs & Controllers;ASP.NET Core Middleware & Request Pipeline
How would you implement custom middleware to catch and log unhandled exceptions before they reach the exception handler, adding request-specific context?
Advanced
A custom try/catch-wrapping middleware placed early in the pipeline can capture and enrich exception context (like the request path, user identity, or a correlation ID) before re-throwing (or short-circuiting with a response) so downstream logging or the exception handler has richer diagnostic information -- though care must be taken not to swallow exceptions unintentionally or duplicate work already handled by UseExceptionHandler/IExceptionHandler.
public class ExceptionEnrichmentMiddleware {
private readonly RequestDelegate _next;
public async Task InvokeAsync(HttpContext context) {
try {
await _next(context);
} catch (Exception ex) {
ex.Data["RequestPath"] = context.Request.Path;
ex.Data["UserId"] = context.User?.Identity?.Name;
throw; // re-throw for the actual exception handler to process
}
}
}
Real-world example
A team adds enrichment middleware that tags every unhandled exception with the requesting user's ID and tenant context before it reaches the global exception handler, making production log entries dramatically more useful for tracing multi-tenant issues.
Common follow-ups: Why is it important to re-throw rather than swallow the exception in this pattern?;How does this compare to just adding this context directly inside a single IExceptionHandler?
Logging;Multiple Inheritance & MRO
What is the difference between handling exceptions with middleware versus using try/catch blocks directly in controller actions?
Intermediate
Middleware-based global exception handling centralizes error-to-response translation logic in one place, applying consistently across every endpoint without needing to remember to add try/catch to each action -- reducing duplication and ensuring uniform error responses. Try/catch in individual actions is appropriate for handling specific, expected exceptions where the action needs to perform particular recovery logic or return a genuinely different response shape for that specific scenario, rather than the generic error handling the global middleware provides.
// Global middleware handles the general case -- no try/catch needed here
[HttpGet("{id}")]
public IActionResult GetProduct(int id) {
var product = _service.GetById(id) ?? throw new NotFoundException();
return Ok(product);
}
// Local try/catch for action-specific recovery logic
[HttpPost("import")]
public async Task<IActionResult> ImportProducts(IFormFile file) {
try {
await _importer.ImportAsync(file);
return Ok();
} catch (InvalidFileFormatException ex) {
return BadRequest(new { ex.Message, SupportedFormats = new[] { "csv", "xlsx" } }); // action-specific response
}
}
Real-world example
A file import endpoint uses a local try/catch to return a highly specific, actionable error response (listing supported formats) for a particular exception type, while relying on the global handler for every other, more generic failure scenario throughout the rest of the API.
Common follow-ups: When does local try/catch handling risk creating inconsistent error response formats?;How do you decide which exceptions deserve action-specific handling versus the global default?
Exception Handling;RESTful Web APIs & Controllers
How does exception handling middleware interact with model validation errors in ASP.NET Core, and how do they typically produce different error response paths?
Advanced
Model validation errors (from [ApiController]'s automatic model state validation, or FluentValidation-style validators) typically don't throw exceptions at all -- instead, [ApiController] automatically short-circuits with a 400 Bad Request and a ValidationProblemDetails response before the action method even executes, entirely bypassing exception-handling middleware since no exception was ever thrown; this is a distinct, separate error path from actual unhandled exceptions, which is why validation errors and unhandled exceptions often need separate consideration when designing a consistent overall error response strategy.
[ApiController] // enables automatic model validation short-circuiting
public class ProductsController : ControllerBase {
[HttpPost]
public IActionResult Create([Required] ProductDto dto) {
// If dto fails validation, this method body never executes --
// [ApiController] already returned a 400 ValidationProblemDetails automatically
return Ok();
}
}
Real-world example
A team designing their unified error response format realizes validation errors (automatic 400 responses) and unhandled exceptions (caught by exception middleware) are two entirely separate code paths that both need to produce a consistent ProblemDetails shape, requiring explicit configuration of both.
Common follow-ups: How do you customize the automatic validation error response format via InvalidModelStateResponseFactory?;How would you unify both paths to guarantee identical response shapes?
RESTful Web APIs & Controllers;Configuration & Options
How should a global exception handler differentiate between exceptions that should return specific client-facing details versus exceptions that should always show a generic message?
Intermediate
A common pattern defines a custom base exception class (like ApiException with a public Message and StatusCode meant for client display) that business logic throws for expected, user-facing error conditions, while any exception NOT inheriting from this base type (unexpected bugs, infrastructure failures) is treated as an internal error and given only a generic message -- this explicit opt-in design prevents accidentally leaking internal exception details for the many exception types never designed to be shown to end users.
public class ApiException : Exception {
public int StatusCode { get; }
public ApiException(string message, int statusCode = 400) : base(message) => StatusCode = statusCode;
}
// In the handler:
var (status, message) = exception is ApiException apiEx
? (apiEx.StatusCode, apiEx.Message) // safe, intentionally client-facing
: (500, "An unexpected error occurred"); // generic for anything else
Real-world example
A checkout flow throws a custom InsufficientStockException : ApiException with a clear customer-facing message, while an unrelated database timeout exception (not inheriting ApiException) is automatically shown only as a generic 'something went wrong' message, preventing internal details from leaking.
Common follow-ups: How do you ensure developers consistently use the ApiException base class for new user-facing errors?;What's the risk of accidentally making ApiException too broadly applicable?
Exception Handling;RESTful Web APIs & Controllers