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
- Use appropriate status codes: Be consistent
- Include error details: But not internals
- Provide error codes: For programmatic handling
- Log server errors: But don't expose logs
- 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.