Search Topics
Search across all FastAPI topics
Custom Handlers
Core ConceptCustom exception handlers let you define exactly how your API responds to specific error types. Create consistent error formats across your entire application.
Your API returns validation errors in Pydantic's default format. Your mobile app expects errors in a different shape. Every error response needs to be translated between two formats. Your frontend team is frustrated.
# Pydantic returns this:
{
"detail": [
{"type": "missing", "loc": ["body", "email"], "msg": "Field required"}
]
}
# Mobile app expects this:
{
"success": false,
"error_code": "VALIDATION_ERROR",
"message": "email is required",
"fields": {"email": "This field is required"}
}
# Frontend team: "Why can't the API just return OUR format?"Question
Sound familiar? Your backend speaks Pydantic, your frontend speaks a different dialect, and every error response needs manual translation. What if you could intercept every error before it leaves your API and reshape it into whatever format your clients expect?
That's exactly what custom exception handlers do. You register a function that says "when THIS type of error happens, return THAT response format."
How Exception Handlers Work
When an exception is raised, FastAPI checks its list of registered handlers. It finds the most specific match and calls that handler to create the response.
Exception raised
raise ItemNotFoundException(42)
FastAPI checks handlers
Most specific match wins
Handler runs
Your function creates the response
Custom JSON response
Your format, your rules
The Pattern
What you just learned
Exception handlers intercept errors BEFORE they become responses
You register handlers with @app.exception_handler(ExceptionType)
The most specific handler wins — ItemNotFound before generic Exception
Every error in your API goes through these handlers, so your format is consistent
Custom Exception Classes
Step one: define your own exception types. These carry domain-specific data — like which item wasn't found, or which permission was denied. Then register a handler that turns them into your API's error format.
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
# Step 1: Define your exception
class ItemNotFoundException(Exception):
def __init__(self, item_id: int):
self.item_id = item_id
app = FastAPI()
# Step 2: Register a handler for it
@app.exception_handler(ItemNotFoundException)
async def item_not_found_handler(
request: Request,
exc: ItemNotFoundException,
):
# Step 3: Return YOUR format
return JSONResponse(
status_code=404,
content={
"success": False,
"error_code": "ITEM_NOT_FOUND",
"message": f"Item {exc.item_id} does not exist",
},
)
# Step 4: Just raise it — the handler does the rest
@app.get("/items/{item_id}")
async def get_item(item_id: int):
if item_id not in database:
raise ItemNotFoundException(item_id) # Clean and readable
return database[item_id]Insight
Notice how clean the endpoint code is? No JSONResponse, no status codes in the route function. You just raise ItemNotFoundException(item_id) and let the handler deal with formatting. Your route functions stay focused on business logic.
Handler Chain in Action
See how FastAPI checks each registered handler in order until one matches the exception type. The most specific handler always wins.
Handler Registration
What you just learned
Register handlers with @app.exception_handler(YourException)
Handlers receive the request and the exception instance
They must return a Response object (usually JSONResponse)
The handler chain checks most-specific to least-specific
Overriding FastAPI's Default Handlers
Here's where it gets really powerful. FastAPI has built-in handlers for validation errors and HTTP exceptions. You can replace them with your own, so every error in your API follows the same format — even Pydantic validation errors.
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
# Override the default HTTP exception handler
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc):
return JSONResponse(
status_code=exc.status_code,
content={
"success": False,
"error_code": f"HTTP_{exc.status_code}",
"message": exc.detail,
},
)
# Override the default validation error handler
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc):
# Transform Pydantic's format into YOUR format
fields = {}
for error in exc.errors():
field_name = error["loc"][-1] # Last element is the field name
fields[field_name] = error["msg"]
return JSONResponse(
status_code=422,
content={
"success": False,
"error_code": "VALIDATION_ERROR",
"message": "Request validation failed",
"fields": fields, # {"email": "field required", "age": "..."}
},
)Watch out
Notice we import StarletteHTTPException, not FastAPI's HTTPException. FastAPI's built-in handler is registered against the Starlette base class. If you register against FastAPI's HTTPException, the built-in handler for the Starlette one still runs. This trips up a lot of people.
Override Defaults
What you just learned
Override StarletteHTTPException to customize ALL HTTP error responses
Override RequestValidationError to reshape Pydantic's validation output
Now every error in your API follows the same JSON structure
Import from starlette.exceptions, not fastapi — this is a common gotcha
Go Deeper: A Unified Error System
Let's solve the exact problem from our opening scenario. Here's how to transform Pydantic's output into the format your mobile team expects.
Pydantic Format vs Mobile App Format
Your mobile team needs a flat 'fields' object with field names as keys and messages as values. Pydantic returns a nested array with 'loc', 'type', and 'msg' keys.
# Default Pydantic validation error response:
# POST /users with body: {}
{
"detail": [
{
"type": "missing",
"loc": ["body", "email"],
"msg": "Field required",
"input": {},
"url": "..."
}
]
}Go Deeper: Logging Inside Handlers
Exception handlers are the perfect place to add logging and monitoring. Every error in your API flows through them, so you get a single place to track, alert, and debug issues.
import logging
logger = logging.getLogger(__name__)
@app.exception_handler(Exception)
async def catch_all_handler(request: Request, exc: Exception):
# Log the error with request context
logger.error(
f"Unhandled error: {exc}",
extra={
"path": request.url.path,
"method": request.method,
"client_ip": request.client.host,
},
exc_info=True, # Include the full traceback
)
# Return a safe response (don't leak internal details!)
return JSONResponse(
status_code=500,
content={
"success": False,
"error_code": "INTERNAL_ERROR",
"message": "An unexpected error occurred",
# Never expose exc details to clients in production!
},
)Watch out
A catch-all Exception handler is your safety net, but never expose the actual error message to clients. Internal errors might contain database connection strings, file paths, or other sensitive information. Log the details server-side, return a generic message to the client.
Think about it...
If you register a handler for Exception (the base class), does it catch HTTPException too?
Hint: Think about handler specificity — what happens when multiple handlers could match?
Key Points
Custom Exceptions
Define domain-specific exception types for clarity
Consistent Format
All errors follow the same JSON structure across your API
Override Defaults
Replace built-in validation and HTTP error handlers
Logging
Add logging and monitoring inside exception handlers