Error Handling and Global Exceptions in FastAPI
Centralized error handling means every failure — a validation error, a raised HTTPException, a domain rule violation, or an unexpected crash — is converted into one predictable, machine-readable response by a small set of registered handlers rather than scattered try/except blocks.
A consistent error contract is part of the public surface of your API, which is why it belongs in Core Architecture and Routing Patterns alongside routing and lifecycle. The handlers are registered in the application factory, they rely on the correlation ID set in middleware, and they feed the observability story covered in Observability and Tracing.
Core Mechanics: Handler Registration
FastAPI lets you register a handler for any exception type. When an exception propagates out of a route, FastAPI matches it against the registered handlers — most specific first — and invokes the matching one to build the response. This is strictly better than per-route try/except, because coverage is guaranteed and the contract lives in one place.
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
class DomainError(Exception):
def __init__(self, code: str, message: str, status_code: int = 400) -> None:
self.code, self.message, self.status_code = code, message, status_code
def register_error_handlers(app: FastAPI) -> None:
@app.exception_handler(DomainError)
async def handle_domain_error(request: Request, exc: DomainError) -> JSONResponse:
# Map a typed domain error to the shared envelope and its status code.
return JSONResponse(
status_code=exc.status_code,
content={"error": exc.code, "message": exc.message,
"request_id": request.headers.get("x-request-id", "")},
)
Production Implementation: Normalizing Validation and Unknowns
Two handlers complete the picture: one normalizes FastAPI's validation errors into your envelope, and one catches everything unforeseen so no raw traceback ever reaches a client.
from fastapi.exceptions import RequestValidationError
import logging
logger = logging.getLogger("api.errors")
def register_catch_all(app: FastAPI) -> None:
@app.exception_handler(RequestValidationError)
async def handle_validation(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content={"error": "validation_error", "message": "Invalid request.",
"details": exc.errors()},
)
@app.exception_handler(Exception)
async def handle_unexpected(request: Request, exc: Exception) -> JSONResponse:
# Full context internally; opaque, stable envelope externally.
logger.error("unhandled", exc_info=exc, extra={"path": request.url.path})
return JSONResponse(
status_code=500,
content={"error": "internal_server_error", "message": "Unexpected error."},
)
The end-to-end implementation, including status-code mapping tables, is detailed in Global Exception Handlers for Consistent API Responses.
Async and Performance Notes
Exception handlers run on the event loop, so keep them light — building a JSON response and emitting a log line is fine, but do not perform additional I/O such as a database write inside a handler on the hot failure path. If you need to persist error events, hand them to a background task rather than blocking the response, as covered in Background Task Processing.
Testing Strategy
Assert both the status code and the envelope shape so the contract is locked:
def test_domain_error_envelope(client):
resp = client.get("/orders/does-not-exist")
assert resp.status_code == 404
body = resp.json()
assert set(body) >= {"error", "message"} # Stable keys clients can rely on.
assert "traceback" not in body # Never leak internals.
Failure Modes and Debugging
- Inconsistent validation shape. Without a
RequestValidationErrorhandler, validation errors return a different format than your other errors. Normalize them. - Swallowed errors. A catch-all that returns 200 hides failures from monitoring. Always preserve the correct status code.
- Leaked internals. Returning
str(exc)can expose secrets or SQL. Return a fixed message and log the detail. - Handler ordering. A handler registered for
Exceptionwill catch subclasses unless a more specific handler exists; register specific handlers too.
Related Reading
- Up to the section: Core Architecture and Routing Patterns.
- Hands-on guide: Global Exception Handlers for Consistent API Responses.
- Composes with: Middleware Implementation and Observability and Tracing.