Error Handling and Logging
Introduction
APIs need predictable errors—404 for missing resources, 422 for validation, 500 without leaking stack traces in production. FastAPI maps HTTPException, Pydantic ValidationError, and custom exceptions to JSON responses. This chapter configures handlers and application logging.
Prerequisites
HTTPException (Recap)
from fastapi import HTTPException
@app.get("/items/{item_id}")
def get_item(item_id: int):
item = find_item(item_id)
if item is None:
raise HTTPException(status_code=404, detail="Item not found")
return itemdetail may be a string or structured dict/list.
Custom Exception Classes
app/core/exceptions.py:
class AppError(Exception):
def __init__(self, message: str, status_code: int = 400, code: int = 1):
self.message = message
self.status_code = status_code
self.code = code
super().__init__(message)Register handler in main.py:
from fastapi import Request
from fastapi.responses import JSONResponse
from app.core.exceptions import AppError
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError):
return JSONResponse(
status_code=exc.status_code,
content={"code": exc.code, "message": exc.message, "data": None},
)Raise in routes:
raise AppError("Email already registered", status_code=409, code=40901)Validation Errors (422)
FastAPI already returns standard 422 for request validation. Customize format:
Use for unified envelope matching success responses.
Global Exception Handler (Production)
Warning
Do Not Expose Tracebacks to Clients
Log full exception server-side; return generic message in production.
Disable or narrow in development if you want FastAPI debug pages—Uvicorn --reload is not a substitute for exposing stacks publicly.
Router-Level Exception Handlers
Register on APIRouter for domain-specific errors—handlers on app apply globally.
Logging Configuration
app/core/logging.py:
Call in lifespan:
@asynccontextmanager
async def lifespan(app: FastAPI):
settings = get_settings()
setup_logging(settings.debug)
yieldFile Logging (Production)
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler("logs/app.log", maxBytes=1_000_000, backupCount=5)
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
logging.getLogger("app").addHandler(handler)Compare Flask logging in error handling chapter.
Log in Routes and Dependencies
Request ID in Logs
Combine with middleware X-Request-ID:
@app.middleware("http")
async def bind_request_id(request: Request, call_next):
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4())[:8])
request.state.request_id = request_id
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return responseInclude request.state.request_id in log extra= dict.
FAQ
HTTPException vs return JSONResponse?
HTTPException integrates with OpenAPI; custom handler for envelope format.
422 vs 400?
FastAPI uses 422 for schema validation; use 400 for business rule failures via HTTPException.
Duplicate log lines?
Check propagate and duplicate handlers on root/uvicorn loggers.
Starlette HTTPException?
Rare—use fastapi.HTTPException in FastAPI apps.
Log sensitive data?
Never log passwords, tokens, or full credit card numbers.
Structured JSON logs?
Use python-json-logger or log extra fields for aggregators.