Clients need consistent, meaningful error responses, and handlers keep the HTTP mapping out of business logic.
To stop a request with an error, raise HTTPException(status_code=404, detail="Item not found"). Raise it rather than return it: it unwinds through helper functions and dependencies, and FastAPI turns it into a JSON response {"detail": "Item not found"} with that status. detail can be any JSON-serializable value, and headers={...} adds response headers.
Pick codes by meaning: 400 for a bad request the validators couldn't catch, 401 when the caller isn't authenticated, 403 when they are but aren't allowed, 404 for a missing resource, 409 for a conflict such as a duplicate email. FastAPI already sends 422 for validation failures, and any unhandled exception becomes a 500 and is logged.
Don't sprinkle HTTP into your service layer, though. Define domain exceptions (class OutOfStock(Exception)) and register a handler with @app.exception_handler(OutOfStock). The handler receives the request and the exception and returns a response, usually a JSONResponse. Now any code that raises OutOfStock during a request, however deep, produces that response. Handlers match by class including subclasses, so one handler for a base AppError can cover a whole hierarchy.
You can also override the built-in handlers:
- A handler for
RequestValidationErrorchanges the format of 422 responses;exc.errors()gives the error list. - A handler for
starlette.exceptions.HTTPExceptioncatches both yourHTTPExceptions and Starlette's own 404 and 405 responses. - To keep the default behaviour but add logging, call
fastapi.exception_handlers.http_exception_handlerfrom your handler.
Keep error bodies consistent across the API; clients write their error handling once.
Example
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
class OutOfStock(Exception):
def __init__(self, sku: str):
self.sku = sku
app = FastAPI()
stock = {"pen": 0, "cup": 3}
@app.exception_handler(OutOfStock)
async def out_of_stock_handler(request: Request, exc: OutOfStock):
return JSONResponse(status_code=409, content={"error": "out_of_stock", "sku": exc.sku})
def reserve(sku: str) -> None: # service code: knows nothing about HTTP
if stock[sku] == 0:
raise OutOfStock(sku)
stock[sku] -= 1
@app.post("/orders/{sku}")
def order(sku: str):
if sku not in stock:
raise HTTPException(status_code=404, detail=f"Unknown product {sku}")
reserve(sku)
return {"ok": True}
c = TestClient(app)
print(c.post("/orders/ink").json()) # {'detail': 'Unknown product ink'}
r = c.post("/orders/pen")
print(r.status_code, r.json()) # 409 {'error': 'out_of_stock', 'sku': 'pen'}Remember
- raise HTTPException(status_code, detail) stops the request from any depth
- 401 = not authenticated, 403 = not allowed, 404 = missing, 409 = conflict
- Map domain exceptions to responses with @app.exception_handler
- Override RequestValidationError's handler to change the 422 format
Watch out for
- Returning HTTPException instead of raising it: FastAPI then tries to serialize the exception object as a normal response.