Search Topics

Search across all FastAPI topics

GitHub

Custom Handlers

Core Concept

Custom 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.

terminal
# 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.

main.py
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.

main.py
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.

Broken code
main.py
# 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.

main.py
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