Isaac.

API Error Handling

Implement consistent error handling and responses in APIs.

By EMEPublished: February 20, 2025
error handlingapi responsesstatus codesvalidation

A Simple Analogy

Good error handling is like clear signage. It tells users exactly what went wrong and how to fix it.


Why Error Handling?

  • Clarity: Users understand what failed
  • Debugging: Easier to fix issues
  • Security: Don't expose internals
  • Consistency: Predictable error format
  • Recovery: Guide users to solutions

HTTP Status Codes

2xx Success
- 200 OK: Request succeeded
- 201 Created: Resource created
- 204 No Content: Success, no body

4xx Client Error
- 400 Bad Request: Invalid input
- 401 Unauthorized: Not authenticated
- 403 Forbidden: No permission
- 404 Not Found: Resource missing
- 409 Conflict: State conflict
- 422 Unprocessable: Validation failed

5xx Server Error
- 500 Internal Server Error: Server fault
- 503 Service Unavailable: Temporarily down

Error Response Format

public class ApiError
{
    public string Code { get; set; }
    public string Message { get; set; }
    public Dictionary<string, string[]> Errors { get; set; }
    public string TraceId { get; set; }
}

// Success response
public class ApiResponse<T>
{
    public T Data { get; set; }
    public bool Success { get; set; }
}

// Controller
[HttpPost]
public async Task<ActionResult<ApiResponse<OrderDto>>> CreateOrder(CreateOrderRequest request)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(new ApiError
        {
            Code = "VALIDATION_ERROR",
            Message = "Validation failed",
            Errors = ModelState
                .Where(x => x.Value.Errors.Count > 0)
                .ToDictionary(x => x.Key, x => x.Value.Errors.Select(e => e.ErrorMessage).ToArray()),
            TraceId = HttpContext.TraceIdentifier
        });
    }
    
    try
    {
        var order = await _service.CreateAsync(request);
        return Ok(new ApiResponse<OrderDto> { Data = order, Success = true });
    }
    catch (ValidationException ex)
    {
        return BadRequest(new ApiError
        {
            Code = "INVALID_ORDER",
            Message = ex.Message,
            TraceId = HttpContext.TraceIdentifier
        });
    }
}

Global Exception Handling

app.UseExceptionHandler(exceptionHandlerApp =>
{
    exceptionHandlerApp.Run(async context =>
    {
        context.Response.ContentType = "application/json";
        
        var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
        var exception = exceptionHandlerPathFeature?.Error;
        
        var response = new ApiError
        {
            TraceId = context.TraceIdentifier,
            Code = "INTERNAL_ERROR",
            Message = "An unexpected error occurred"
        };
        
        context.Response.StatusCode = exception switch
        {
            ArgumentException => StatusCodes.Status400BadRequest,
            UnauthorizedAccessException => StatusCodes.Status401Unauthorized,
            KeyNotFoundException => StatusCodes.Status404NotFound,
            InvalidOperationException => StatusCodes.Status409Conflict,
            _ => StatusCodes.Status500InternalServerError
        };
        
        if (context.Response.StatusCode == StatusCodes.Status500InternalServerError)
        {
            _logger.LogError(exception, "Unhandled exception");
        }
        
        await context.Response.WriteAsJsonAsync(response);
    });
});

Problem Details (RFC 7807)

// Modern standard error format
var problemDetails = new ProblemDetails
{
    Type = "https://api.example.com/errors/validation-error",
    Title = "Validation Failed",
    Status = StatusCodes.Status400BadRequest,
    Detail = "The order contains invalid items",
    Instance = HttpContext.Request.Path
};

problemDetails.Extensions.Add("errors", new
{
    items = new[] { "Items list is empty" },
    customerId = new[] { "CustomerId is required" }
});

return BadRequest(problemDetails);

Best Practices

  1. Use appropriate status codes: Be consistent
  2. Include error details: But not internals
  3. Provide error codes: For programmatic handling
  4. Log server errors: But don't expose logs
  5. Document errors: Show expected errors in API docs

Related Concepts

  • API documentation
  • Validation frameworks
  • Logging and monitoring
  • Security best practices

Summary

Implement consistent error responses with appropriate status codes, clear messages, and error codes. Use global exception handling for uniform error responses.