In API development, error handling is crucial for ensuring system robustness and user experience. When a user request encounters issues (e.g., parameter errors, resource not found), proper error handling allows the API to return clear prompts, helping callers quickly identify problems. FastAPI provides a concise and efficient error handling mechanism, enabling flexible use of HTTP status codes while gracefully capturing and processing exceptions.
I. Why Error Handling Matters?¶
Imagine if a user requests a non-existent user ID and the API crashes or returns a blank response—users would be highly confused. Correct error handling can:
- Return a clear error reason to the caller (e.g., “User not found”)
- Use standard HTTP status codes (e.g., 404, 400) for easier parsing by frontends or other systems
- Prevent program crashes due to unhandled exceptions, ensuring service stability
II. HTTP Status Codes: API “Traffic Lights”¶
HTTP status codes form the foundation of error handling. FastAPI supports all standard HTTP status codes. Common codes and scenarios are as follows:
| Status Code | Meaning | Use Case |
|---|---|---|
| 200 | Success | Request processed normally, return data |
| 400 | Bad Request | Invalid parameters (e.g., format error) |
| 404 | Not Found | Requested resource (e.g., user, product) does not exist |
| 401 | Unauthorized | Requires authentication but no valid credentials provided |
| 403 | Forbidden | Insufficient permissions |
| 422 | Validation Error | Valid parameters but failed validation |
| 500 | Internal Server Error | Server-side logic error (e.g., database connection failure) |
Using Status Codes in FastAPI: In route functions, specify status codes directly via return or raise. For example:
from fastapi import FastAPI, HTTPException
app = FastAPI()
# Successful response (default 200)
@app.get("/health")
def health_check():
return {"status": "OK"} # Automatically returns 200 status code
# Explicitly return 404 status code
@app.get("/items/{item_id}")
def get_item(item_id: int):
if item_id not in [1, 2, 3]: # Assume only 1, 2, 3 exist
return {"status": "error", "detail": "Item not found"}, 404 # Return status code directly
III. FastAPI Exception Handling: Proactively “Throwing” Errors¶
FastAPI recommends using the HTTPException class to proactively throw errors. It allows specifying status codes and error messages directly, making error handling clearer.
1. Basic Usage: HTTPException¶
from fastapi import HTTPException
@app.get("/users/{user_id}")
def get_user(user_id: int):
# Simulate database query
users = {1: "Alice", 2: "Bob"}
if user_id not in users:
# Raise HTTPException with status code and details
raise HTTPException(
status_code=404, # Resource not found
detail=f"User ID {user_id} does not exist", # Error description
headers={"X-Error-Reason": "User not found"} # Optional response headers
)
return {"user_id": user_id, "name": users[user_id]}
Effect: When requesting /users/999, the API returns:
{
"detail": "User ID 999 does not exist"
}
The status code is 404, so frontends can identify “resource not found” based on this code.
2. Parameter Validation Errors: Auto-Return 422¶
FastAPI automatically handles parameter format errors (e.g., type mismatch) and returns a 422 status code. For example:
@app.get("/users/{user_id}")
def get_user(user_id: int): # Enforce user_id as integer
# If user passes a non-integer (e.g., "abc"), FastAPI automatically intercepts and returns 422
return {"user_id": user_id}
Effect: When requesting /users/abc, the API returns:
{
"detail": [
{
"loc": ["path", "user_id"],
"msg": "Input value is not a valid integer",
"type": "type_error.integer"
}
]
}
The status code is 422 (validation error), including specific error fields and reasons.
IV. Custom Exceptions: “Domain-Specific Errors” for Business Logic¶
For business logic errors (e.g., “insufficient funds,” “permission denied”), custom exception classes can be defined and managed uniformly via FastAPI’s exception handling mechanism.
1. Define Custom Exceptions¶
class InsufficientFundsError(Exception):
"""Custom exception: Insufficient funds"""
def __init__(self, balance: float, needed: float):
self.balance = balance # Current balance
self.needed = needed # Required amount
self.detail = f"Insufficient funds. Current balance: {balance}, Needed: {needed}"
2. Global Exception Handling¶
Register custom exception handlers with @app.exception_handler for uniform global processing:
from fastapi import Request, status
from fastapi.responses import JSONResponse
# Handle InsufficientFundsError
@app.exception_handler(InsufficientFundsError)
async def handle_insufficient_funds(request: Request, exc: InsufficientFundsError):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST, # 400 status code
content={"detail": exc.detail} # Return custom error message
)
3. Use Custom Exceptions in Routes¶
@app.post("/withdraw")
def withdraw(amount: float):
balance = 100.0 # Assume current balance
if amount > balance:
# Raise custom exception
raise InsufficientFundsError(balance=balance, needed=amount)
return {"message": "Withdrawal successful", "balance": balance - amount}
Effect: When requesting /withdraw?amount=200, the API returns:
{
"detail": "Insufficient funds. Current balance: 100.0, Needed: 200.0"
}
V. Global Error Handling: Unified “Fallback” Strategy¶
For unforeseen exceptions (e.g., database connection failures), global exception handling avoids repetitive code and ensures standardized error returns.
from fastapi import Request, status
from fastapi.responses import JSONResponse
# Handle all uncaught exceptions (general fallback)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
# Log the error (use logging in production environments)
print(f"Unhandled exception: {str(exc)}")
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"detail": "Internal server error. Please contact the administrator"}
)
VI. Best Practices Summary¶
- Use HTTPException for standard HTTP errors: Handle 404 (resource not found), 400 (parameter error), 422 (validation failure), etc., by raising exceptions with specified status codes.
- Custom exceptions for business logic: Use
exception_handlerto unify returns for domain-specific errors (e.g., “insufficient funds”). - Leverage automatic parameter validation: FastAPI handles type errors automatically, returning 422 status codes without manual intervention.
- Global exception handling: Use
@app.exception_handlerto unify fallbacks for uncaught exceptions, reducing duplicate code.
VII. Integrated Example Code¶
from fastapi import FastAPI, HTTPException, Request, status
from fastapi.responses import JSONResponse
app = FastAPI()
# Simulated user data
users = {1: "Alice", 2: "Bob"}
# Custom exception: Insufficient Funds
class InsufficientFundsError(Exception):
def __init__(self, balance: float, needed: float):
self.balance = balance
self.needed = needed
self.detail = f"Insufficient funds. Current balance: {balance}, Needed: {needed}"
# Global exception handler: Fallback for all uncaught exceptions
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
print(f"Global error: {str(exc)}")
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"detail": "Internal server error"}
)
# Custom exception handler: Insufficient Funds
@app.exception_handler(InsufficientFundsError)
async def handle_insufficient_funds(request: Request, exc: InsufficientFundsError):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"detail": exc.detail}
)
# 1. Get User (handle 404 errors)
@app.get("/users/{user_id}")
def get_user(user_id: int):
if user_id not in users:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User ID {user_id} does not exist"
)
return {"user_id": user_id, "name": users[user_id]}
# 2. Withdrawal API (handle custom exception)
@app.post("/withdraw")
def withdraw(amount: float):
balance = 100.0 # Assume current balance
if amount > balance:
raise InsufficientFundsError(balance=balance, needed=amount)
return {"message": "Withdrawal successful", "remaining": balance - amount}
By mastering these techniques, you can build robust and user-friendly APIs using FastAPI: standardize errors with HTTP status codes, handle scenarios with HTTPException and custom exceptions, and ensure uniform fallbacks via global exception handling.