Errors: HTTPException and exception handlers

FastAPI · Practical · 12 min

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 RequestValidationError changes the format of 422 responses; exc.errors() gives the error list.
  • A handler for starlette.exceptions.HTTPException catches both your HTTPExceptions and Starlette's own 404 and 405 responses.
  • To keep the default behaviour but add logging, call fastapi.exception_handlers.http_exception_handler from 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.

Check yourself

  1. Why is raising HTTPException better than returning an error dict from a helper function?
  2. You register a handler for RequestValidationError. What changes?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: fastapi.tiangolo.com/tutorial/handling-errors/