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.