Exception Handling Middleware
đ¯ What You'll Learn
- Built-in exception handling middleware
- Developer Exception Page
- UseExceptionHandler middleware
- Custom exception handling middleware
- Logging exceptions
- Returning JSON error responses
- Problem Details (RFC 7807)
- Production-ready error handling
Why Exception Handling Matters
Without proper exception handling:
- Users see ugly error pages
- Sensitive information leaks (stack traces, connection strings)
- Errors go unlogged
- APIs return HTML instead of JSON
Never expose detailed error information in production! Stack traces can reveal:
- File paths and directory structure
- Database connection strings
- Third-party library versions
- Business logic details
1. Developer Exception Page
In development, use the Developer Exception Page for detailed error information:
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
}
What It Shows
- Full stack trace
- Exception message
- Request headers
- Query string
- Cookies
- Routing data
In ASP.NET Core 6+, UseDeveloperExceptionPage() is added automatically
in development. You don't need to call it explicitly.
2. UseExceptionHandler Middleware
In production, use UseExceptionHandler to handle errors gracefully:
app.UseExceptionHandler("/error");
// Define error endpoint
app.MapGet("/error", () => Results.Problem());
With Custom Error Page
[ApiController]
public class ErrorController : ControllerBase
{
[Route("/error")]
[ApiExplorerSettings(IgnoreApi = true)]
public IActionResult HandleError()
{
return Problem(
title: "An error occurred",
statusCode: 500);
}
}
3. Custom Exception Handling Middleware
For full control, create custom exception handling middleware:
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionMiddleware> _logger;
private readonly IHostEnvironment _environment;
public GlobalExceptionMiddleware(
RequestDelegate next,
ILogger<GlobalExceptionMiddleware> logger,
IHostEnvironment environment)
{
_next = next;
_logger = logger;
_environment = environment;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "An unhandled exception occurred");
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = 500;
var response = new
{
error = "An error occurred while processing your request",
details = _environment.IsDevelopment() ? exception.Message : null,
stackTrace = _environment.IsDevelopment() ? exception.StackTrace : null
};
await context.Response.WriteAsJsonAsync(response);
}
}
Registration
app.UseMiddleware<GlobalExceptionMiddleware>();
4. Problem Details (RFC 7807)
Use the Problem Details standard for consistent API error responses:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-abc123...",
"errors": {
"Name": ["The Name field is required."]
}
}
Enable Problem Details
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseExceptionHandler();
app.UseStatusCodePages();
Custom Problem Details
public class ProblemDetailsExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ProblemDetailsExceptionMiddleware> _logger;
public ProblemDetailsExceptionMiddleware(
RequestDelegate next,
ILogger<ProblemDetailsExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
_logger.LogError(exception, "An error occurred: {Message}", exception.Message);
var (statusCode, title) = exception switch
{
ArgumentException => (400, "Bad Request"),
UnauthorizedAccessException => (401, "Unauthorized"),
KeyNotFoundException => (404, "Not Found"),
_ => (500, "Internal Server Error")
};
var problemDetails = new ProblemDetails
{
Status = statusCode,
Title = title,
Detail = exception.Message,
Instance = context.Request.Path
};
problemDetails.Extensions["traceId"] = context.TraceIdentifier;
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(problemDetails);
}
}
5. Logging Exceptions
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
// Log with structured data
_logger.LogError(
exception,
"Unhandled exception: {ExceptionType} | Path: {Path} | Method: {Method} | User: {User}",
exception.GetType().Name,
context.Request.Path,
context.Request.Method,
context.User.Identity?.Name ?? "Anonymous");
// Return error response...
}
Complete InvenTrack Example
public class InvenTrackExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<InvenTrackExceptionMiddleware> _logger;
private readonly IHostEnvironment _environment;
public InvenTrackExceptionMiddleware(
RequestDelegate next,
ILogger<InvenTrackExceptionMiddleware> logger,
IHostEnvironment environment)
{
_next = next;
_logger = logger;
_environment = environment;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
// Determine status code and title based on exception type
var (statusCode, title, logLevel) = exception switch
{
ArgumentException => (400, "Bad Request", LogLevel.Warning),
UnauthorizedAccessException => (401, "Unauthorized", LogLevel.Warning),
KeyNotFoundException => (404, "Not Found", LogLevel.Warning),
InvalidOperationException => (409, "Conflict", LogLevel.Warning),
_ => (500, "Internal Server Error", LogLevel.Error)
};
// Log the exception
_logger.Log(
logLevel,
exception,
"[InvenTrack] {ExceptionType} | {Method} {Path} | User: {User} | TraceId: {TraceId}",
exception.GetType().Name,
context.Request.Method,
context.Request.Path,
context.User.Identity?.Name ?? "Anonymous",
context.TraceIdentifier);
// Build Problem Details response
var problemDetails = new ProblemDetails
{
Status = statusCode,
Title = title,
Type = $"https://httpstatuses.com/{statusCode}",
Instance = context.Request.Path
};
// Add detail only in development
if (_environment.IsDevelopment())
{
problemDetails.Detail = exception.Message;
problemDetails.Extensions["stackTrace"] = exception.StackTrace;
}
else
{
problemDetails.Detail = "An error occurred while processing your request.";
}
// Add trace ID
problemDetails.Extensions["traceId"] = context.TraceIdentifier;
// Add correlation ID if available
if (context.Items.TryGetValue("X-Correlation-Id", out var correlationId))
{
problemDetails.Extensions["correlationId"] = correlationId;
}
// Set response
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(problemDetails);
}
}
Extension Method
public static IApplicationBuilder UseInvenTrackExceptionHandler(
this IApplicationBuilder app)
{
return app.UseMiddleware<InvenTrackExceptionMiddleware>();
}
Usage in Program.cs
var app = builder.Build();
// Exception handling (FIRST!)
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseInvenTrackExceptionHandler();
app.UseHsts();
}
// Rest of pipeline...
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Example Response
{
"type": "https://httpstatuses.com/404",
"title": "Not Found",
"status": 404,
"detail": "An error occurred while processing your request.",
"instance": "/api/products/999",
"traceId": "00-abc123def456...",
"correlationId": "xyz789"
}
{
"type": "https://httpstatuses.com/404",
"title": "Not Found",
"status": 404,
"detail": "Product with ID 999 not found",
"instance": "/api/products/999",
"traceId": "00-abc123def456...",
"correlationId": "xyz789",
"stackTrace": " at InvenTrack.Services.ProductService..."
}
Best Practices
- Exception handler first: Always register exception handling middleware first
- Never expose stack traces in production: Security risk
- Use Problem Details: Standard format for API errors
- Log all exceptions: Include context (user, path, trace ID)
- Map exception types to status codes: ArgumentException â 400, etc.
- Include trace/correlation IDs: Help debugging
- Different responses for dev/prod: Details in dev, generic in prod
- Use structured logging: Makes searching logs easier
- Don't catch everything: Let critical errors bubble up
- Test error handling: Verify responses and logging
Key Takeaways
- Developer Exception Page: Detailed errors in development
- UseExceptionHandler: Production error handling
- Custom middleware: Full control over error responses
- Problem Details (RFC 7807): Standard error format
- Map exception types to HTTP status codes
- Log exceptions with context (user, path, trace ID)
- Never expose sensitive information in production
- Include trace/correlation IDs for debugging
- Different error details for dev vs production
- Exception handler must be first in the pipeline
Congratulations! You've completed Part V: Middleware & Pipeline. You now understand:
- â The request pipeline and how it works
- â Built-in middleware (static files, CORS, auth, etc.)
- â Creating custom middleware (inline, convention-based, factory-based)
- â Middleware ordering and why it matters
- â Exception handling middleware and best practices
You're building a solid foundation in ASP.NET Core! The middleware pipeline is the backbone of every ASP.NET Core application. Keep up the great work! đ