Validation and Error Handling

FastAPI validates inputs automatically and raises HTTPException for deliberate errors. Learning the error flow is key to building APIs that fail clearly instead of crashing.

HTTPException: The Standard Way

from fastapi import FastAPI, HTTPException

app = FastAPI()

@app.get("/items/{item_id}")
def read_item(item_id: int):
    if item_id not in items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return items_db[item_id]

Raising HTTPException(404, "Item not found") produces the response {"detail": "Item not found"} with status 404. detail can be any JSON-serializable value, including a dict or list.

Custom Headers on Errors

raise HTTPException(
    status_code=403,
    detail="Not enough permissions",
    headers={"WWW-Authenticate": "Bearer"},
)

Automatic Validation Errors (422)

When request data does not match the declared types/constraints, FastAPI returns 422 before your function runs:

{
  "detail": [
    {
      "type": "greater_than",
      "loc": ["path", "item_id"],
      "msg": "Input should be greater than 0",
      "input": "-5"
    }
  ]
}

loc tells you where the problem is: path, query, body (nested as body.field.subfield).

Custom Exception Handlers

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

app = FastAPI()

@app.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError):
    return JSONResponse(
        status_code=422,
        content={"errors": exc.errors(), "body": exc.body.decode() if isinstance(exc.body, bytes) else exc.body},
    )

class UnicornException(Exception):
    pass

@app.exception_handler(UnicornException)
async def unicorn_handler(request: Request, exc: UnicornException):
    return JSONResponse(status_code=418, content={"message": f"Oops: {exc}"})

Pydantic Validators for Business Rules

from pydantic import BaseModel, field_validator

class Order(BaseModel):
    quantity: int
    price: float

    @field_validator("quantity")
    @classmethod
    def quantity_positive(cls, v: int) -> int:
        if v <= 0:
            raise ValueError("quantity must be positive")
        return v

Use model validators for data-shaped rules (formats, ranges, cross-field checks); use HTTPException inside endpoints for resource-state rules (does the item exist?, is the user allowed?).

Quick Questions and Answers

Q: When should I use HTTPException vs returning an error dict?

A: Always raise. Returning {"error": ...} with status 200 breaks clients that check the status code and pollutes the docs. Exceptions also stop execution immediately.

Q: How do I return a different status code for validation errors?

A: Override the RequestValidationError handler and return JSONResponse(status_code=400, ...) instead of 422, or use FastAPI(exception_handlers={...}) at app creation.

Q: Where do unhandled exceptions go?

A: They bubble up to the server, which returns a generic 500 and logs the traceback. Catch what you can meaningfully handle; let programming bugs crash loudly in dev, and add monitoring (Sentry, OpenTelemetry) in production.

Conclusion

FastAPI's error model has three layers: automatic 422 validation errors for malformed input, HTTPException for expected application errors, and custom exception handlers for shaping responses. Together they give clients precise, consistent failure information.