Difficulty: Intermediate
Error handling is a critical part of API design. Well-structured error responses help clients understand what went wrong and how to fix it. FastAPI provides several mechanisms for error handling, from the simple HTTPException to custom exception classes with registered handlers.
HTTPException is the primary way to return error responses in FastAPI. It accepts a status_code, a detail message (string or any JSON-serializable data), and optional headers. When raised inside a handler, FastAPI catches it and returns the appropriate error response. The detail field can be a string for simple errors or a dictionary/list for structured error data.
For consistent error formats across your API, you can create custom exception classes and register exception handlers using @app.exception_handler(). This lets you standardize the error response structure - for example, always including an 'error_code', 'message', and 'details' field. The handler receives the Request and the exception, and returns a Response.
FastAPI automatically handles Pydantic validation errors, returning a 422 status code with detailed information about which fields failed validation and why. You can override the default validation error handler to customize the format. The default format includes the error location (query, path, or body), the field name, the type of error, and a human-readable message.
The RequestValidationError is raised by FastAPI when request validation fails. You can import it from fastapi.exceptions and register a custom handler. This is useful for transforming the verbose default validation errors into a simpler format that matches your API's error convention.
A robust error handling strategy includes: consistent error response format, proper HTTP status codes, actionable error messages, unique error codes for programmatic handling, and logging of server errors for debugging. Never expose internal details (like stack traces or database queries) in error responses.
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/users/{user_id}")
def get_user(user_id: int):
if user_id < 1:
raise HTTPException(
status_code=400,
detail="User ID must be a positive integer",
)
if user_id > 1000:
raise HTTPException(
status_code=404,
detail="User not found",
headers={"X-Error": "user-not-found"},
)
return {"id": user_id, "name": "Alice"}
# Structured error detail
@app.post("/transfer")
def transfer(amount: float):
if amount <= 0:
raise HTTPException(
status_code=400,
detail={
"error_code": "INVALID_AMOUNT",
"message": "Transfer amount must be positive",
"field": "amount",
"received": amount,
},
)
return {"transferred": amount}
HTTPException's detail can be a string or a structured object. Structured errors are more useful for clients that need to programmatically handle different error types. Custom headers can carry machine-readable error codes.
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
class AppError(Exception):
def __init__(self, code: str, message: str, status: int = 400):
self.code = code
self.message = message
self.status = status
class NotFoundError(AppError):
def __init__(self, resource: str, resource_id: str):
super().__init__(
code=f"{resource.upper()}_NOT_FOUND",
message=f"{resource} with id '{resource_id}' not found",
status=404,
)
class ConflictError(AppError):
def __init__(self, message: str):
super().__init__(code="CONFLICT", message=message, status=409)
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError):
return JSONResponse(
status_code=exc.status,
content={
"error": exc.code,
"message": exc.message,
"path": str(request.url),
},
)
@app.get("/users/{user_id}")
def get_user(user_id: int):
raise NotFoundError(resource="User", resource_id=str(user_id))
@app.post("/users")
def create_user():
raise ConflictError(message="Email already registered")
A base AppError class standardizes error attributes. Subclasses like NotFoundError and ConflictError fill in specific details. A single exception handler catches all AppError subclasses and formats them consistently.
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(RequestValidationError)
async def validation_error_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"],
"type": error["type"],
})
return JSONResponse(
status_code=422,
content={
"error": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": errors,
},
)
from pydantic import BaseModel, Field
class UserCreate(BaseModel):
name: str = Field(min_length=1)
age: int = Field(ge=0)
@app.post("/users")
def create_user(user: UserCreate):
return user
# POST /users with body: {"name": "", "age": -1}
# Custom response:
# {
# "error": "VALIDATION_ERROR",
# "message": "Request validation failed",
# "details": [
# {"field": "body.name", "message": "...", "type": "..."}
# ]
# }
Overriding the RequestValidationError handler lets you customize the validation error format. The default format is verbose - this custom handler simplifies it to field, message, and type. The exc.errors() method returns a list of all validation errors.
HTTPException, Custom Exceptions, Exception Handlers, Error Format, Validation Errors