
FastAPI
Learn the fundamentals of error practices in FastAPI.
Error handling best practices ensure your API is reliable, maintainable, and provides excellent debugging information. Following established patterns helps you handle edge cases consistently and build robust systems.
In this section, you'll understand:
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel
import logging
logger = logging.getLogger(__name__)
app = FastAPI()
class ValidationResult:
def __init__(self, is_valid: bool, errors: list = None):
self.is_valid = is_valid
self.errors = errors or []
class UserCreate(BaseModel):
username: str
email: str
age: int
def validate_user(user: UserCreate) -> ValidationResult:
errors = []
if len(user.username) < 3:
errors.append("Username must be at least 3 characters")
if "@" not in user.email:
errors.append("Invalid email format")
if user.age < 18:
errors.append("Must be at least 18 years old")
return ValidationResult(len(errors) == 0, errors)
@app.post("/users/", status_code=status.HTTP_201_CREATED)
async def create_user(user: UserCreate):
# Validate early
validation = validate_user(user)
if not validation.is_valid:
logger.warning(f"User validation failed for {user.username}")
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail={"errors": validation.errors}
)
# Check for duplicates
if db.user_exists(user.email):
logger.warning(f"User already exists: {user.email}")
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="User with this email already exists"
)
try:
# Create user
new_user = db.create_user(user)
logger.info(f"User created: {new_user.id}")
return new_user
except Exception as e:
logger.error(f"Failed to create user {user.email}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create user"
)❌ DON'T: Expose implementation details
@app.get("/items/{item_id}")
async def get_item(item_id: int):
try:
return db.query("SELECT * FROM items WHERE id = ?", item_id)
except Exception as e:
return JSONResponse(status_code=500, content={"detail": str(e)})✅ DO: Return generic messages in production
@app.get("/items/{item_id}")
async def get_item(item_id: int):
try:
return db.query("SELECT * FROM items WHERE id = ?", item_id)
except Exception as e:
logger.error(f"Database error: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal server error"
)from enum import Enum
from typing import Optional
class ErrorSeverity(str, Enum):
INFO = "info"
WARNING = "warning"
ERROR = "error"
CRITICAL = "critical"
class AppError(Exception):
def __init__(
self,
code: str,
message: str,
status_code: int,
severity: ErrorSeverity = ErrorSeverity.ERROR,
user_message: Optional[str] = None
):
self.code = code
self.message = message
self.status_code = status_code
self.severity = severity
self.user_message = user_message or message
# Specific exceptions
class ResourceNotFoundError(AppError):
def __init__(self, resource: str, resource_id):
super().__init__(
code="NOT_FOUND",
message=f"{resource} {resource_id} not found",
status_code=404,
user_message=f"{resource} not found"
)
class InvalidInputError(AppError):
def __init__(self, field: str, reason: str):
super().__init__(
code="INVALID_INPUT",
message=f"Invalid value for field '{field}': {reason}",
status_code=422,
user_message=f"Invalid {field}: {reason}"
)
class AuthenticationError(AppError):
def __init__(self):
super().__init__(
code="UNAUTHORIZED",
message="Authentication required",
status_code=401,
user_message="Please provide valid credentials"
)
@app.exception_handler(AppError)
async def app_error_handler(request, exc):
logger.log(
logging.getLevelName(exc.severity),
f"{exc.code}: {exc.message}"
)
return JSONResponse(
status_code=exc.status_code,
content={
"error": exc.code,
"message": exc.user_message,
"timestamp": datetime.utcnow().isoformat()
}
)
@app.get("/users/{user_id}")
async def get_user(user_id: int):
if user_id < 1:
raise InvalidInputError("user_id", "Must be positive")
user = db.get_user(user_id)
if not user:
raise ResourceNotFoundError("User", user_id)
return userProper error practices enable:
Example - Financial transaction API:
class InsufficientFundsError(AppError):
def __init__(self, required: float, available: float):
super().__init__(
code="INSUFFICIENT_FUNDS",
message=f"Insufficient funds. Required: {required}, Available: {available}",
status_code=402,
severity=ErrorSeverity.WARNING,
user_message="Your account has insufficient funds"
)
@app.post("/transfer")
async def transfer(transfer: TransferRequest):
# Validate early
if transfer.amount <= 0:
raise InvalidInputError("amount", "Must be greater than 0")
# Check funds
account = db.get_account(transfer.from_id)
if account.balance < transfer.amount:
raise InsufficientFundsError(transfer.amount, account.balance)
try:
# Execute transfer
db.transfer(transfer.from_id, transfer.to_id, transfer.amount)
logger.info(f"Transfer successful: {transfer.amount}")
return {"status": "success"}
except Exception as e:
logger.error(f"Transfer failed: {str(e)}", exc_info=True)
raise AppError(
code="TRANSFER_FAILED",
message="Transfer could not be completed",
status_code=500,
user_message="Transfer failed. Please try again."
)Ready to explore more? Check out the advanced section for production patterns and edge cases.
Resources
Ojasa Mirai
Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.
Learn Deep • Build Real • Verify Skills • Launch Forward