Global Exception Handlers
Introduction
A global exception handler is a function that runs whenever a specific exception type is raised anywhere in your FastAPI app. It transforms the exception into an HTTP response in one place, so route functions stay focused on the happy path.
Why This Matters
Repeating the same try/except in every route quickly becomes noise. Global handlers let you say once "if this exception occurs, return that response", and FastAPI applies it everywhere. They are the right place for cross-cutting concerns like consistent error envelopes, logging, and observability.
Registering a Handler
Use the @app.exception_handler decorator with the exception class to handle:
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
class ItemNotFound(Exception):
def __init__(self, item_id: int):
self.item_id = item_id
@app.exception_handler(ItemNotFound)
async def item_not_found_handler(request: Request, exc: ItemNotFound):
return JSONResponse(
status_code=404,
content={"message": f"Item {exc.item_id} does not exist"},
)
@app.get("/items/{item_id}")
def get_item(item_id: int):
if item_id != 1:
raise ItemNotFound(item_id)
return {"item_id": item_id, "name": "Pen"}Any route in the app that raises ItemNotFound now produces a uniform 404 response.
Customizing the Built-In Validation Errors
FastAPI's default 422 response is verbose. Override it with a handler for RequestValidationError:
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content={
"message": "Invalid input",
"errors": [
{"field": ".".join(str(p) for p in err["loc"]), "reason": err["msg"]}
for err in exc.errors()
],
},
)Now every validation failure returns a consistent shape across your API.
Overriding HTTPException
You can also customize the response for HTTPException itself:
from fastapi import HTTPException
from fastapi.exception_handlers import http_exception_handler
@app.exception_handler(HTTPException)
async def custom_http_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content={
"status": exc.status_code,
"message": exc.detail,
"path": request.url.path,
},
headers=exc.headers,
)This is how you implement an "error envelope" without rewriting every route.
Catching Unhandled Exceptions
Register a handler for the bare Exception class to catch anything unexpected:
import logging
logger = logging.getLogger(__name__)
@app.exception_handler(Exception)
async def unhandled_exception_handler(request: Request, exc: Exception):
logger.exception("Unhandled error", exc_info=exc)
return JSONResponse(
status_code=500,
content={"message": "Internal server error"},
)This guarantees a clean response even when something genuinely unexpected happens. Always log the original exception so you can debug later.
Multiple Handlers and Specificity
You can register several handlers. FastAPI picks the most specific match based on the exception class hierarchy:
class AppError(Exception): ...
class NotFoundError(AppError): ...
class PermissionError(AppError): ...
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError): ...
@app.exception_handler(NotFoundError)
async def not_found_handler(request: Request, exc: NotFoundError): ...NotFoundError uses its own handler. PermissionError falls back to the AppError handler.
Accessing the Request
The handler receives the Request object so you can include path, headers, or query data in the response or in logs:
@app.exception_handler(ItemNotFound)
async def item_not_found_handler(request: Request, exc: ItemNotFound):
logger.warning("Missing item", extra={
"path": request.url.path,
"client": request.client.host if request.client else "unknown",
})
return JSONResponse(status_code=404, content={"message": "Not found"})Where to Put Handlers
For small apps, defining handlers in main.py is fine. For larger projects, group them in their own module and register them inside an init_app function:
def register_exception_handlers(app: FastAPI) -> None:
app.add_exception_handler(ItemNotFound, item_not_found_handler)
app.add_exception_handler(RequestValidationError, validation_handler)
app.add_exception_handler(Exception, unhandled_exception_handler)add_exception_handler is the imperative equivalent of the decorator.
Common Mistakes
Swallowing exceptions silently
A handler that returns 200 OK or logs nothing makes bugs invisible. Always log unexpected errors and return a non-2xx status.
Catching too broadly too early
Registering a handler for Exception before more specific ones is fine - FastAPI still matches the most specific class. But avoid catching exceptions inside route functions just to ignore them.
Forgetting headers from HTTPException
If you override the HTTPException handler, remember to forward exc.headers. Otherwise things like WWW-Authenticate for 401 will go missing.
Summary
Global exception handlers translate exceptions into HTTP responses in one place. Use them to standardize error shapes, override default validation errors, and catch unexpected failures. Pair them with logging so problems remain visible while clients still get a clean response.
How is this guide?
Last updated on
