Complete DevOps Bootcamp: Master DevOps in 12 Weeks
FastAPIResponse Handling and Errors

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