Skip to content

Chapter 9 · Error Handling and Custom Exceptions ​

Chapter goals: Master the correct usage of HTTPException, learn to register custom exception handlers, override default validation error responses, and design a unified error response structure.

9.1 Use raise, Not return ​

When a client error occurs, return an HTTP status code in the 400–499 range. FastAPI's idiomatic approach is to raise HTTPException:

python
from fastapi import FastAPI, HTTPException

app = FastAPI()

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


@app.get("/items/{item_id}")
async def read_item(item_id: str):
    if item_id not in items:
        # Note: it is raise, not return
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item": items[item_id]}

Why raise? Because HTTPException is a ordinary Python exception — it can bubble up from deep utility functions: whenever any layer raises it, the current request terminates immediately and the error response is sent to the client; remaining path function code is skipped. With return, you would need to manually pass and check at every layer.

The detail parameter accepts not just strings — any JSON-serializable value (dict, list, etc.) appears in the response body under the "detail" key.

9.2 Adding Response Headers and Custom Exception Handlers ​

Some scenarios require custom headers on error responses (typical: WWW-Authenticate on auth failures):

python
raise HTTPException(
    status_code=401,
    detail="Session expired",
    headers={"WWW-Authenticate": "Bearer"},
)

For business-domain error types that recur, the more elegant approach is to define your own exception class and register a global handler:

python
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse


class PaymentError(Exception):
    """Business custom exception: payment failed"""

    def __init__(self, reason: str, order_id: str):
        self.reason = reason
        self.order_id = order_id


app = FastAPI()


@app.exception_handler(PaymentError)          # Register: hand this exception to this handler
async def payment_error_handler(
    request: Request, exc: PaymentError
):
    return JSONResponse(
        status_code=402,                      # Payment Required — pick as needed
        content={
            "code": "PAYMENT_FAILED",
            "message": exc.reason,
            "order_id": exc.order_id,
        },
    )


@app.post("/pay/{order_id}")
async def pay(order_id: str):
    raise PaymentError("Insufficient balance", order_id)   # Business code just raises; handler catches uniformly

This keeps business code clean: no try/except in every endpoint; exception handling strategy is centralized in one place.

::: note Technical detail These exception utilities come directly from Starlette; fastapi.responses.JSONResponse is simply a convenience alias for the Starlette同名 class. :::

9.3 Overriding Default Exception Handlers ​

FastAPI ships two built-in default handlers: one for HTTPException and one for RequestValidationError (request data validation failures). Both can be overridden.

Customizing the Validation Error Response Format ​

Frontend teams often find the default 422 response too verbose (nested loc/msg/type array). You can replace it entirely:

python
from fastapi import FastAPI, Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse

app = FastAPI()


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(
    request: Request, exc: RequestValidationError
):
    errors = []
    for error in exc.errors():
        errors.append({
            "field": ".".join(str(loc) for loc in error["loc"]),
            "message": error["msg"],
        })
    return JSONResponse(
        status_code=422,
        content={"detail": errors},
    )

9.4 Chapter Summary ​

  • raise HTTPException(status_code, detail=...) is the idiomatic way to return client errors;
  • Custom exception classes + @app.exception_handler() for domain-specific errors;
  • Override RequestValidationError handler to customize 422 response format;
  • detail accepts any JSON-serializable value; headers= adds response headers.

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. Why use raise HTTPException instead of return a dict with status_code?

2. What does @app.exception_handler(MyException) do?

3. What is the benefit of overriding the RequestValidationError handler?

4. Can detail in HTTPException be a dict?

🛠️ Hands-On Exercises ​

  1. Write a route that raises HTTPException(status_code=404, detail={"code": "ITEM_NOT_FOUND"}) when an item is missing; verify the response JSON.
  2. Define a custom NotFoundError(Exception) and register @app.exception_handler(NotFoundError) to return a unified {"code": "...", "message": "..."} shape.
  3. Override the RequestValidationError handler to flatten the 422 response to {"errors": [{"field": "...", "msg": "..."}]}; test by sending an invalid request.

Proceed to Chapter 10: Dependency Injection Basics.