Last modified: Dec 02, 2025 By Alexander Williams

FastAPI Error Handling Guide

Building APIs means handling errors. FastAPI provides powerful tools for this.

Good error handling improves user experience and API reliability.

This guide covers everything from basic errors to custom exception handlers.

Why Error Handling Matters

APIs fail. Clients send bad data. Servers encounter issues.

Proper error handling tells clients what went wrong. It provides clear messages.

This prevents confusion and helps with debugging. It is crucial for production apps.

Basic Error Handling with HTTPException

FastAPI's HTTPException is your primary tool. Import it from fastapi.

Raise it inside your path operations. It immediately stops execution.

It returns an HTTP status code and a detail message to the client.


from fastapi import FastAPI, HTTPException

app = FastAPI()

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

If a client requests /items/0, they get a 404 error.

The response body contains the detail message. It is JSON formatted.


{
  "detail": "Item not found"
}

Adding Custom Headers

Sometimes you need to send extra info. Use the headers parameter.

This is useful for authentication errors or rate limiting.


raise HTTPException(
    status_code=403,
    detail="Insufficient permissions",
    headers={"X-Error": "Forbidden-Access"}
)

Request Validation Errors

FastAPI automatically validates request data. It uses Pydantic models.

Invalid data triggers a 422 Unprocessable Entity error.

The error response is detailed. It shows exactly what failed.


from pydantic import BaseModel

class Item(BaseModel):
    name: str
    price: float

@app.post("/items/")
async def create_item(item: Item):
    return item

Sending invalid JSON or wrong types triggers validation.


# Request with missing 'price'
{
  "name": "Widget"
}

# Response
{
  "detail": [
    {
      "loc": ["body", "price"],
      "msg": "field required",
      "type": "value_error.missing"
    }
  ]
}

Custom Exception Handlers

You can catch any Python exception. Use app.add_exception_handler.

This gives you full control over the response. You can log errors here.

It is perfect for unexpected server errors (500).


from fastapi import Request
from fastapi.responses import JSONResponse

@app.exception_handler(ValueError)
async def value_error_handler(request: Request, exc: ValueError):
    return JSONResponse(
        status_code=400,
        content={"message": f"A value error occurred: {exc}"},
    )

@app.get("/divide/{a}/{b}")
async def divide(a: float, b: float):
    if b == 0:
        raise ValueError("Division by zero is not allowed")
    return {"result": a / b}

Now, division by zero returns a clean 400 error.

Overriding Default Handlers

You can also override FastAPI's default handlers. Like the 422 validation error.

This lets you customize the format of automatic validation responses.


from fastapi.exceptions import RequestValidationError

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    # Custom logic to reformat the errors
    errors = exc.errors()
    simplified_errors = []
    for error in errors:
        simplified_errors.append({
            "field": error["loc"][-1],
            "problem": error["msg"]
        })
    return JSONResponse(
        status_code=422,
        content={"errors": simplified_errors},
    )

Global Exception Catching

Use a handler for the base Exception class. It catches everything unhandled.

This is your safety net. Always return a generic 500 error to the client.

Never expose internal details like stack traces.


import logging

logging.basicConfig(level=logging.ERROR)

@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):
    # Log the full error for server-side debugging
    logging.error(f"Unhandled exception: {exc}", exc_info=True)
    # Send a generic message to the client
    return JSONResponse(
        status_code=500,
        content={"message": "An internal server error occurred."},
    )

Structured Error Responses

Define a consistent error response model. Use a Pydantic BaseModel.

This ensures all your errors look the same. It helps API consumers.


from pydantic import BaseModel
from typing import Optional

class ErrorResponse(BaseModel):
    error: str
    code: Optional[str] = None
    details: Optional[dict] = None

# Use it in a custom handler
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
    error_response = ErrorResponse(
        error=exc.detail,
        code="HTTP_ERROR",
        details={"status_code": exc.status_code}
    )
    return JSONResponse(
        status_code=exc.status_code,
        content=error_response.dict()
    )

Integrating with Database Operations

Database calls often fail. Handle these errors gracefully.

For example, when using an async database setup, connection errors can occur.

Catch specific database exceptions. Return appropriate HTTP status codes.


from sqlalchemy.exc import NoResultFound, IntegrityError

@app.exception_handler(NoResultFound)
async def no_result_found_handler(request: Request, exc: NoResultFound):
    raise HTTPException(status_code=404, detail="Resource not found")

@app.exception_handler(IntegrityError)
async def integrity_error_handler(request: Request, exc: IntegrityError):
    # e.g., duplicate key violation
    raise HTTPException(status_code=409, detail="Data conflict occurred")

Error Handling in Dependencies

Dependencies can raise errors too. They are caught by the same exception handlers.

This is useful for authentication and authorization checks. Learn more in our guide on dependency injection best practices.


from fastapi import Depends, Header

async def verify_token(x_token: str = Header(...)):
    if x_token != "secret-token":
        raise HTTPException(status_code=401, detail="Invalid token")
    return x_token

@app.get("/secure/", dependencies=[Depends(verify_token)])
async def secure_route():
    return {"message": "Access granted"}

Testing Your Error Handlers

Write tests for your error responses. Use FastAPI's TestClient.

Ensure your handlers are triggered correctly. Check status codes and messages.


from fastapi.testclient import TestClient

client = TestClient(app)

def test_item_not_found():
    response = client.get("/items/0")
    assert response.status_code == 404
    assert response.json()["detail"] == "Item not found"

def test_validation_error():
    response = client.post("/items/", json={"name": "Widget"}) # missing price
    assert response.status_code == 422

Logging for Debugging

Log errors with context. Include the request ID and user info if available.

This is vital for debugging in production. It helps track down issues fast.

Consider using structured logging for better searchability.

Conclusion

Effective error handling makes your API robust and user-friendly.

Use HTTPException for client errors. Create custom handlers for specific exceptions.

Always catch unexpected errors with a global handler. Return consistent, secure responses.

Combine this with good practices like performance optimization for a top-tier API.

Test your error paths thoroughly. Your users and your debugging self will thank you.