13. Global Error Handling
- Global Error Handling
About this chapter
Implement centralized error handling middleware to catch exceptions, log errors, and return consistent error responses across your API.
- Middleware pipeline order: How middleware wraps and executes
- Exception handling middleware: Catching unhandled exceptions
- Global exception handler: Custom middleware for centralized error handling
- Error response formatting: Consistent error message structure
- Logging exceptions: Capturing stack traces and context
- Environment-based responses: Different details for dev vs production
Learning outcomes:
- Understand middleware execution order and importance
- Create custom exception handling middleware
- Return standardized error responses
- Log exceptions with appropriate detail levels
- Handle specific exception types differently
- Prevent sensitive information leaks in production responses
13.1 Understanding Middleware Pipeline
// Program.cs - Middleware order matters!
var app = builder.Build();
// 1. Exception handling (must be first)
app.UseExceptionHandler("/error");
// 2. HTTPS redirection
app.UseHttpsRedirection();
// 3. Routing
app.UseRouting();
// 4. CORS (before authentication)
app.UseCors("AllowSpecificOrigins");
// 5. Authentication (who are you?)
app.UseAuthentication();
// 6. Authorization (what can you do?)
app.UseAuthorization();
// 7. Endpoints
app.MapControllers();app.Run();
- Order Importance: Earlier middleware wraps later middleware
- Exception Handler: Catches unhandled exceptions from entire pipeline
13.2 NEW: Creating Global Exception Handling Middleware
namespace CommandAPI.Middleware;
public class GlobalExceptionHandlerMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionHandlerMiddleware> _logger;
private readonly IHostEnvironment _environment;
public GlobalExceptionHandlerMiddleware(
RequestDelegate next,
ILogger<GlobalExceptionHandlerMiddleware> 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";
var problemDetails = new ProblemDetails
{
Instance = context.Request.Path
};
switch (exception)
{
case BadHttpRequestException badRequestException:
problemDetails.Status = StatusCodes.Status400BadRequest;
problemDetails.Title = "Bad Request";
problemDetails.Detail = badRequestException.Message;
break;
case KeyNotFoundException:
problemDetails.Status = StatusCodes.Status404NotFound;
problemDetails.Title = "Resource Not Found";
problemDetails.Detail = exception.Message;
break;
case UnauthorizedAccessException:
problemDetails.Status = StatusCodes.Status401Unauthorized;
problemDetails.Title = "Unauthorized";
problemDetails.Detail = "Authentication is required";
break;
case DbUpdateException dbUpdateException:
problemDetails.Status = StatusCodes.Status409Conflict;
problemDetails.Title = "Database Conflict";
problemDetails.Detail = "A database constraint was violated";
// Don't expose internal errors in production
if (_environment.IsDevelopment())
{
problemDetails.Detail = dbUpdateException.Message;
}
break;
default:
problemDetails.Status = StatusCodes.Status500InternalServerError;
problemDetails.Title = "Internal Server Error";
problemDetails.Detail = _environment.IsDevelopment()
? exception.Message
: "An error occurred processing your request";
break;
}
context.Response.StatusCode = problemDetails.Status.Value;
// Add stack trace in development
if (_environment.IsDevelopment())
{
problemDetails.Extensions["stackTrace"] = exception.StackTrace;
problemDetails.Extensions["innerException"] = exception.InnerException?.Message;
}
await context.Response.WriteAsJsonAsync(problemDetails);
}
}
13.3 NEW: Implementing ProblemDetails (RFC 7807)
// Standard error response format
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
"title": "Resource Not Found",
"status": 404,
"detail": "No platform exists with ID 999",
"instance": "/api/platforms/999",
"traceId": "00-abc123-def456-00"
}
- Benefits:
- Standardized error format
- Machine-readable
- Human-readable
- Extensible (custom properties)
- Industry standard (RFC 7807)
// Using ProblemDetails in controllers
[HttpGet("{id}")]
public async Task<ActionResult<PlatformReadDto>> GetPlatformById(int id)
{
var platform = await _repository.GetPlatformByIdAsync(id);
if (platform == null)
{
return NotFound(new ProblemDetails
{
Status = StatusCodes.Status404NotFound,
Title = "Platform Not Found",
Detail = $"No platform exists with ID {id}",
Instance = HttpContext.Request.Path, T
ype = "https://api.example.com/errors/not-found"
});
}
return Ok(_mapper.Map<PlatformReadDto>(platform));}
13.4 Consistent Error Responses
// Create helper class for consistent errors
public static class ProblemDetailsFactory
{
public static ProblemDetails NotFound(string resource, object id, HttpContext context)
{
return new ProblemDetails
{
Status = StatusCodes.Status404NotFound,
Title = $"{resource} Not Found",
Detail = $"No {resource.ToLower()} exists with ID {id}",
Instance = context.Request.Path,
Type = "https://api.example.com/errors/not-found"
};
}
public static ProblemDetails ValidationError(
string title,
Dictionary<string, string[]> errors,
HttpContext context)
{
var problemDetails = new ProblemDetails
{
Status = StatusCodes.Status400BadRequest,
Title = title,
Instance = context.Request.Path,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1"
};
problemDetails.Extensions["errors"] = errors;
return problemDetails;
}
}
// Usage
if (platform == null)
{
return NotFound(ProblemDetailsFactory.NotFound("Platform", id, HttpContext));
}
13.5 CORRECTION: Properly Implementing AuthenticationErrorMiddleware
// Current code has this middleware but it's commented out!
// //app.UseMiddleware<AuthenticationErrorMiddleware>();namespace CommandAPI.Security;
public class AuthenticationErrorMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<AuthenticationErrorMiddleware> _logger;
public AuthenticationErrorMiddleware(
RequestDelegate next,
ILogger<AuthenticationErrorMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
await _next(context);
// Check for authentication failures
if (context.Response.StatusCode == StatusCodes.Status401Unauthorized)
{
var problemDetails = new ProblemDetails
{
Status = StatusCodes.Status401Unauthorized,
Title = "Unauthorized",
Detail = "Authentication credentials are missing or invalid",
Instance = context.Request.Path
};
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(problemDetails);
}
else if (context.Response.StatusCode == StatusCodes.Status403Forbidden)
{
var problemDetails = new ProblemDetails
{
Status = StatusCodes.Status403Forbidden,
Title = "Forbidden",
Detail = "You do not have permission to access this resource",
Instance = context.Request.Path
};
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(problemDetails);
}
}
}
// Enable in Program.cs
app.UseMiddleware<AuthenticationErrorMiddleware>();
13.6 Development vs Production Error Details
// Conditional error detail exposure
if (_environment.IsDevelopment())
{
problemDetails.Extensions["exception"] = exception.ToString();
problemDetails.Extensions["stackTrace"] = exception.StackTrace;
problemDetails.Extensions["data"] = exception.Data;
}
else
{
// Production: generic message, log details
_logger.LogError(exception, "Error processing request {Path}",
context.Request.Path);
problemDetails.Detail = "An error occurred. Please contact support with trace ID: "
context.TraceIdentifier;
}
- Security: Never expose internal details in production
- Logging: Always log full details server-side
- Trace ID: Include for support troubleshooting