Skip to content

Handling errors

FastAPI answers HTTPException and validation errors with its default handlers, and lets you register your own with @app.exception_handler. The binary keeps Starlette's error-handling layers in the same order, so a handler catches the same exceptions and an unhandled one ends in the same 500.

HTTPException with a detail and headers, a custom exception with its handler, and an unhandled exception. The handler is registered on the application:

docs_src/tutorial/errors.py
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import JSONResponse

router = APIRouter(prefix="/errors", tags=["errors"])

ITEMS = {"foo": "The Foo Wrestlers"}


@router.get("/items/{item_id}")
async def read_item(item_id: str):
    if item_id not in ITEMS:
        raise HTTPException(status_code=404, detail="Item not found", headers={"X-Error": "missing item"})
    return {"item": ITEMS[item_id]}


class UnicornException(Exception):
    def __init__(self, name: str):
        self.name = name


# registered on the application: app.add_exception_handler(UnicornException, unicorn_handler)
async def unicorn_handler(request: Request, exc: UnicornException):
    return JSONResponse(status_code=418, content={"message": f"Oops! {exc.name} did something."})


@router.get("/unicorns/{name}")
async def read_unicorn(name: str):
    if name == "yolo":
        raise UnicornException(name)
    return {"unicorn": name}


@router.get("/divide")
async def divide(a: int, b: int):
    return {"result": a / b}  # b=0: an unhandled exception, FastAPI's plain 500
docs_src/main.py (excerpt)
app.add_exception_handler(errors.UnicornException, errors.unicorn_handler)

Like every example on this site, it is compiled and compared with FastAPI in CI (how).

What is native

  • Starlette's stack: ServerErrorMiddleware (the Exception/500 handler, whose response bypasses user middleware; the exception is still logged) → user middleware (last added runs first) → ExceptionMiddleware (handlers by status code, then by the exception's MRO; FastAPI's defaults for HTTPException and RequestValidationError) → router (404/405 raised as HTTPException, hence catchable). A route's RequestValidationError carries FastAPI's exc.body (the decoded JSON body, its bytes, or None); on a form route it is not set (reading it is an AttributeError).
  • @app.exception_handler(class | code), app.add_exception_handler(class | code, handler), also when they are registered by a configure(app) function (Middleware).
  • HTTPException(code) without detail: the http.HTTPStatus phrase of CPython ≥ 3.13 ("Content Too Large", "Unprocessable Content"), like Starlette on those versions.
  • traceback.format_exception(...) / format_exception_only(exc): the exception's own line (module.Class: message), as CPython formats it for a traceback of None; exc.__traceback__ is None (the binary has no Python frames), so frames and chained exceptions are not rendered.
  • FastAPI's default handlers called from an application's own: await http_exception_handler(request, exc) and await request_validation_exception_handler(request, exc) (fastapi.exception_handlers).

Validation errors (RequestValidationError, ValidationError.errors()) are described in Models; exception classes of the project (class attributes, methods, super().__init__ of HTTPException) in Python semantics. What the body of a 500 contains is in Security § Errors and logs.

What stays in Python

Nothing specific.