FastAPI/
Lesson

Error handling is the difference between an APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. that developers trust and one that makes them lose hours to debugging. When your API returns a clear, consistent error response, the frontend developer knows exactly what went wrong. When it returns a raw Python traceback or a vague "Internal Server Error," everyone suffers.

FastAPI provides excellent error handling primitives. The problem is that AI-generated code rarely uses them correctly, defaulting to patterns that work in development but are dangerous in production.

HTTPException basics

FastAPI's HTTPException is the standard way to signal an error from a route handlerWhat is route handler?A Next.js file named route.js inside the app/ directory that handles HTTP requests directly - the App Router equivalent of API routes.. You raise it like a regular Python exception, and FastAPI catches it and converts it into an HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. response.

from fastapi import FastAPI, HTTPException

app = FastAPI()

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    user = await find_user(user_id)
    if not user:
        raise HTTPException(
            status_code=404,
            detail="User not found"
        )
    return user

The response body for a raised HTTPException looks like this:

json
{
  "detail": "User not found"
}

You can also pass a dictionary as detail for richer error information:

raise HTTPException(
    status_code=404,
    detail={
        "message": "User not found",
        "user_id": user_id,
        "suggestion": "Check that the user ID is correct"
    }
)

And you can add custom headers to the error response:

raise HTTPException(
    status_code=401,
    detail="Invalid token",
    headers={"WWW-Authenticate": "Bearer"}
)
Good to know
HTTPException from FastAPI is different from Starlette's HTTPException. FastAPI's version accepts any JSON-serializable detail, while Starlette's only accepts strings. Always import from fastapi, not starlette.
02

Custom exception handlers

Sometimes you want to handle exceptions that are not HTTPException, a database connection error, a third-party APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. timeout, or your own custom exception classes. That is where @app.exception_handler comes in.

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

class ItemOutOfStockError(Exception):
    def __init__(self, item_id: str, requested: int, available: int):
        self.item_id = item_id
        self.requested = requested
        self.available = available

app = FastAPI()

@app.exception_handler(ItemOutOfStockError)
async def out_of_stock_handler(request: Request, exc: ItemOutOfStockError):
    return JSONResponse(
        status_code=409,
        content={
            "error": "out_of_stock",
            "message": f"Item {exc.item_id} has {exc.available} units available, "
                       f"but {exc.requested} were requested",
            "item_id": exc.item_id,
            "available": exc.available,
        },
    )

Now any route handlerWhat is route handler?A Next.js file named route.js inside the app/ directory that handles HTTP requests directly - the App Router equivalent of API routes. can simply raise ItemOutOfStockError(...) and the custom handler converts it into a clean 409 response. The route handler code stays focused on business logic, and error formatting is centralized.

You can register handlers for any exception type, including Python built-in exceptions:

@app.exception_handler(ValueError)
async def value_error_handler(request: Request, exc: ValueError):
    return JSONResponse(
        status_code=400,
        content={
            "error": "validation_error",
            "message": str(exc),
        },
    )
03

Pydantic validation errors

FastAPI uses Pydantic to validate request bodies, query parameters, and path parameters. When validation fails, FastAPI automatically returns a 422 Unprocessable Entity response with detailed error information.

from pydantic import BaseModel, Field

class CreateUser(BaseModel):
    name: str = Field(min_length=1, max_length=100)
    email: str
    age: int = Field(ge=0, le=150)

@app.post("/users")
async def create_user(user: CreateUser):
    return {"message": f"Created {user.name}"}

If you send {"name": "", "email": "test@example.com", "age": -5}, FastAPI returns:

json
{
  "detail": [
    {
      "type": "string_too_short",
      "loc": ["body", "name"],
      "msg": "String should have at least 1 character",
      "input": "",
      "ctx": {"min_length": 1}
    },
    {
      "type": "greater_than_equal",
      "loc": ["body", "age"],
      "msg": "Input should be greater than or equal to 0",
      "input": -5,
      "ctx": {"ge": 0}
    }
  ]
}

This is extremely detailed, almost too detailed. The loc array tells you exactly where the error is (body, query, path), the type gives a machine-readable error code, and msg gives a human-readable explanation.

The problem is that this format is different from your HTTPException error format. Your 404 returns {"detail": "User not found"} (a string), while your 422 returns {"detail": [...]} (an array). Frontend developers have to handle two different shapes.

04

Designing a consistent error shape

Production APIs need a single, predictable error format. Every error, whether it is a 400, 404, 422, or 500, should return the same JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. structure. This lets frontend developers write one error handling function instead of many.

Here is a common pattern:

from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError

app = FastAPI()

# Override the default HTTPException handler
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
    return JSONResponse(
        status_code=exc.status_code,
        content={
            "error": True,
            "status": exc.status_code,
            "message": exc.detail if isinstance(exc.detail, str) else str(exc.detail),
            "request_id": getattr(request.state, "request_id", None),
        },
    )

# Override the default validation error handler
@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"],
            "type": error["type"],
        })
    return JSONResponse(
        status_code=422,
        content={
            "error": True,
            "status": 422,
            "message": "Validation failed",
            "details": errors,
            "request_id": getattr(request.state, "request_id", None),
        },
    )

# Catch-all for unhandled exceptions
@app.exception_handler(Exception)
async def unhandled_exception_handler(request: Request, exc: Exception):
    # Log the full error server-side
    logger.error(
        "unhandled_exception",
        extra={
            "error": str(exc),
            "traceback": traceback.format_exc(),
            "path": request.url.path,
            "request_id": getattr(request.state, "request_id", None),
        },
    )
    # Return a clean response to the client
    return JSONResponse(
        status_code=500,
        content={
            "error": True,
            "status": 500,
            "message": "An internal error occurred",
            "request_id": getattr(request.state, "request_id", None),
        },
    )

Now every error response has the same shape:

FieldTypeDescription
errorbooleanAlways true for error responses
statusintegerHTTP status code (redundant but convenient for the client)
messagestringHuman-readable error description
detailsarray (optional)Field-level validation errors, only present on 422
request_idstring (optional)Correlation ID for tracing in logs
05

How AI handles errors, and why it is dangerous

When you ask AI to "add error handling" to a FastAPI app, here is what you typically get:

# AI-generated error handling - DO NOT use in production
@app.exception_handler(Exception)
async def generic_handler(request: Request, exc: Exception):
    return JSONResponse(
        status_code=500,
        content={
            "error": str(exc),
            "traceback": traceback.format_exc(),
            "path": str(request.url),
        },
    )

This has three serious problems:

Stack traces in responses. The traceback.format_exc() call returns the full Python traceback, including file paths, line numbers, and sometimes sensitive data like database connection strings or APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. keys. This is an information disclosure vulnerability. Attackers can use stack traces to understand your application structure, find vulnerable dependencies, and craft targeted attacks.

Everything becomes a 500. The catch-all handler returns 500 for every unhandled exception, even when the real cause is a 400 (bad input) or a 503 (downstream service unavailable). This makes monitoring useless, you cannot tell real server errors from client mistakes.

No correlation ID. Without a request ID in the error response, when a user reports "I got an error," you have no way to find the corresponding log entry. You are left searching through thousands of log lines hoping to match timestamps.

AI pitfall
AI almost always includes stack traces in error responses. In development, this feels helpful. In production, it is a security vulnerability. Always log the traceback server-side and return a generic message to the client.
06

Error handling hierarchy

FastAPI checks exception handlers in a specific order:

  1. Exact type match: if you registered a handler for ItemOutOfStockError, it catches only that exact type
  2. Parent class match: if no exact handler exists, FastAPI walks up the inheritance chain
  3. Default handlers: HTTPException and RequestValidationError have built-in handlers that you can override
  4. Catch-all: a handler for Exception catches everything that no other handler matched

This means you can layer your handlers from specific to generic:

@app.exception_handler(ItemOutOfStockError)     # Most specific
@app.exception_handler(BusinessLogicError)       # Category
@app.exception_handler(HTTPException)             # Framework errors
@app.exception_handler(RequestValidationError)    # Validation errors
@app.exception_handler(Exception)                 # Catch-all (last resort)
07

Production checklist for error handling

Before deploying a FastAPI application, verify these points:

CheckWhy it matters
No stack traces in responsesInformation disclosure vulnerability
All errors return the same JSON shapeFrontend developers need one error handler, not five
500 handler logs the full traceback server-sideYou need the details for debugging, just not in the response
Every error response includes a request IDEnables tracing from user report to server log
Validation errors (422) include field-level detailsFrontend needs to know which fields failed
Custom business exceptions have dedicated handlersPrevents catch-all from swallowing meaningful errors