Technical
Error Handling Patterns for Production APIs
Your API works perfectly with valid data. Then a user sends an empty string, a missing field, or a 50MB request body, and the whole thing crashes with a 500 Internal Server Error. Proper error handling is the difference between a toy project and a production system.
The Error Hierarchy
Not all errors are equal. Your API should handle them differently:
400 Bad Request: Client sent invalid data (validation failure)
401 Unauthorized: Client is not authenticated
403 Forbidden: Client is authenticated but not authorized
404 Not Found: Requested resource does not exist
409 Conflict: Resource already exists (duplicate creation)
422 Unprocessable: Data is valid format but fails business rules
429 Too Many: Rate limit exceeded
500 Internal Error: Something broke on our side (bug)The Golden Rule
4xx errors are the client's fault. 5xx errors are your fault. If a client can trigger a 500 error with any input, that is a bug in your code, not a problem with their request.
FastAPI Error Handling Patterns
Pattern 1: Pydantic Validation (Automatic)
FastAPI handles validation errors automatically. When a request does not match the Pydantic model, the client gets a 422 with details:
class PostCreate(BaseModel):
title: str = Field(min_length=1, max_length=200)
body: str = Field(min_length=10)
status: Literal['draft', 'published'] = 'draft'Sending {"title": ""} returns: 422: title must have at least 1 character.
Pattern 2: HTTPException for Business Logic
from fastapi import HTTPException
@router.get('/posts/{slug}')
async def get_post(slug: str):
post = find_post_by_slug(slug)
if not post:
raise HTTPException(
status_code=404,
detail=f'Post not found: {slug}'
)
return postPattern 3: Custom Exception Handlers
@app.exception_handler(DuplicateSlugError)
async def handle_duplicate(request, exc):
return JSONResponse(
status_code=409,
content={'detail': str(exc)}
)Error Response Format
Use a consistent error response format across all endpoints:
{
"detail": "Post not found: invalid-slug",
"status_code": 404,
"error_type": "not_found"
}Consistent format means frontend developers can write one error handler that works for every endpoint. Inconsistent formats mean special-case handling everywhere.
The Catch-All Handler
Always add a global exception handler for unexpected errors:
@app.exception_handler(Exception)
async def handle_unexpected(request, exc):
# Log the full error for debugging
logger.error(f'Unexpected error: {exc}', exc_info=True)
# Return a generic message to the client
return JSONResponse(
status_code=500,
content={'detail': 'Internal server error'}
)Never expose stack traces, database errors, or internal details to clients. Log them for debugging, return a generic message to the client.
See the FastAPI error handling documentation for the complete reference.
RELATED READING
The Consulting Shift I Am Making In Year Two
After a year of writing and building, my consulting practice is changing shape. Shorter engagements. Sharper outcomes.
ReadThe Frontend Shift: Shipping Less JavaScript In Year Two
A year ago I reached for Next.js for everything. This year I often reach for nothing.
ReadThe Serverless Lesson I Would Write On A Sticky Note
After a year of shipping serverless projects, one rule explains most of the wins and all of the losses.
Read