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:
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):
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:
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 uniformlyThis 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:
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
RequestValidationErrorhandler to customize 422 response format; detailaccepts 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
- Write a route that raises
HTTPException(status_code=404, detail={"code": "ITEM_NOT_FOUND"})when an item is missing; verify the response JSON. - Define a custom
NotFoundError(Exception)and register@app.exception_handler(NotFoundError)to return a unified{"code": "...", "message": "..."}shape. - Override the
RequestValidationErrorhandler to flatten the 422 response to{"errors": [{"field": "...", "msg": "..."}]}; test by sending an invalid request.
Proceed to Chapter 10: Dependency Injection Basics.