13. Global Error Handling

  1. 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