# Errors Use handlers and dependencies to signal HTTP meaning clearly, and let FastAPI or Starlette own the wire format. ## Why this convention exists FastAPI sits at a boundary where several kinds of failures meet: - invalid requests - permission failures - missing resources - domain or infrastructure errors - framework/runtime exceptions Good code keeps those concerns separate. The stable pattern is: - handlers and dependencies raise semantic HTTP exceptions for boundary failures - request validation errors come from FastAPI and Pydantic automatically - exception handlers turn those failures into HTTP responses - tests assert the HTTP contract, not private exception plumbing That keeps route code small and error responses consistent. ## The convention 1. Raise `HTTPException` for request-level failures such as 400, 401, 403, and 404. 2. Let FastAPI and Pydantic produce validation errors instead of hand-rolling error dicts. 3. Centralize error-envelope formatting in exception handlers when the API needs a stable shape. 4. Keep internal exception details out of client responses unless that disclosure is part of the contract. 5. Keep domain exceptions framework-agnostic and translate them at the HTTP boundary. ## Follow it at the HTTP boundary Use this whenever the failure is about what the caller sent, what the caller can access, or what the API contract should look like on failure. ## Avoid these mistakes Do *not* raise `HTTPException` from deep reusable domain code. Do *not* build ad hoc error JSON in each route. Do *not* catch every exception just to expose `str(exc)` to clients. ## Preferred shape ```python @router.get("/{id}") def read_item(session: SessionDep, current_user: CurrentUser, id: UUID) -> ItemPublic: item = session.get(Item, id) if not item: raise HTTPException(status_code=404, detail="Item not found") if item.owner_id != current_user.id: raise HTTPException(status_code=403, detail="Not enough permissions") return item ``` Why this works: - the failure is explicit at the boundary - the status code carries the meaning - the framework serializes it consistently ## Counterexamples ### Returning error dicts from handlers ```python return {"error": "not_found", "message": "Item not found"} ``` Bad because success and failure flows blur together and status handling becomes inconsistent. ### Framework exceptions in core domain code ```python class BillingService: def charge(...): raise HTTPException(status_code=402, detail="Payment required") ``` Bad because domain code is now coupled to one web framework. ### Catch-all 500 handling that leaks internals ```python except Exception as exc: raise HTTPException(status_code=500, detail=str(exc)) ``` Bad because it leaks internal details and destroys useful operational distinctions. ## Source signals ### FastAPI - `fastapi/exception_handlers.py:11-26` is the core citation here: `HTTPException` becomes a JSON `{"detail": ...}` response when a body is allowed, and `RequestValidationError` becomes a 422 response using `exc.errors()`. - `docs_src/bigger_applications/app_an_py310/routers/items.py:21-25` raises `HTTPException(status_code=404, detail="Item not found")` for a missing resource. - `docs_src/bigger_applications/app_an_py310/routers/items.py:33-37` raises `HTTPException(status_code=403, detail=...)` for a forbidden update. ### Full-stack FastAPI Template - `backend/app/api/deps.py:30-45` translates invalid token, missing user, and inactive user states into semantic HTTP errors inside a dependency. - `backend/app/api/routes/items.py:53-57` uses 404 and 403 in the read handler. - `backend/app/api/routes/items.py:86-95` repeats the same boundary pattern on update before applying the mutation. - `backend/app/api/routes/items.py:106-112` repeats it on delete before committing the write. ### Pydantic and Starlette - `pydantic/docs/index.md:109-152` shows invalid input producing structured validation errors rather than ad hoc strings. - `starlette/tests/test_applications.py:218-238` asserts 404, custom 405, and 500 responses at the HTTP layer, reinforcing that response behavior is a framework contract, not just an implementation detail. - `starlette/applications.py:35-48` separates middleware and exception handlers in application setup, which is part of why centralized formatting works cleanly. ## Bottom line Routes and dependencies should express *HTTP meaning*. Exception handlers should own *HTTP formatting*. Core logic should stay framework-agnostic until the boundary translates it.