Lesson 11 · FastAPI deep dive
FastAPI Tutorial #11: Error Handling & Custom Exceptions
Video eleven of the eighteen-part series: turning failures into consistent, useful responses. HTTPException recap, custom exception classes, and overriding…
- CourseFastAPI deep dive
- Lesson11 of 12
- Video18 min
- FormatJupyter notebook · 10 code cells
What you'll learn
- HTTPException - the Quick Recap
- A Custom Exception Class
- app.exceptionhandler - Turning It Into a Response
- Overriding the Validation Error Handler
- Overriding the HTTPException Handler
- A Family of Related Custom Exceptions
- A Catch-All Handler for Unexpected Exceptions
- HTTPException headers - for Things Like WWW-Authenticate
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbFastAPI Deep-Dive, Video 11: Error Handling#
- Video eleven of the eighteen-part series: turning failures into consistent, useful responses.
- HTTPException recap, custom exception classes, and overriding the built-in error handlers.
- Let's get into it.
Part 1: HTTPException - the Quick Recap#
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
app = FastAPI()
ITEMS = {1: 'Mug', 2: 'Plate'}
@app.get('/items/{item_id}')
def get_item(item_id: int):
if item_id not in ITEMS:
raise HTTPException(status_code=404, detail='item not found')
return {'id': item_id, 'name': ITEMS[item_id]}
client = TestClient(app)
print(client.get('/items/1').json())
print(client.get('/items/99').json())
Part 2: A Custom Exception Class#
class ItemNotFoundError(Exception):
def __init__(self, item_id: int):
self.item_id = item_id
print(issubclass(ItemNotFoundError, Exception))
Part 3: app.exception_handler - Turning It Into a Response#
from fastapi import Request
from fastapi.responses import JSONResponse
app_v2 = FastAPI()
@app_v2.exception_handler(ItemNotFoundError)
async def item_not_found_handler(request: Request, exc: ItemNotFoundError):
return JSONResponse(status_code=404, content={'detail': f'item {exc.item_id} not found'})
@app_v2.get('/items-v2/{item_id}')
def get_item_v2(item_id: int):
if item_id not in ITEMS:
raise ItemNotFoundError(item_id)
return {'id': item_id, 'name': ITEMS[item_id]}
client_v2 = TestClient(app_v2)
print(client_v2.get('/items-v2/1').json())
print(client_v2.get('/items-v2/99').json())
Part 4: Overriding the Validation Error Handler#
from fastapi.exceptions import RequestValidationError
from pydantic import BaseModel
app_v3 = FastAPI()
class Product(BaseModel):
name: str
price: float
@app_v3.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError):
return JSONResponse(status_code=422, content={'error': 'validation_failed', 'field_count': len(exc.errors())})
@app_v3.post('/products')
def create_product(product: Product):
return product.model_dump()
client_v3 = TestClient(app_v3)
print(client_v3.post('/products', json={'name': 'Mug'}).json())
Part 5: Overriding the HTTPException Handler#
app_v4 = FastAPI()
@app_v4.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
return JSONResponse(status_code=exc.status_code, content={'error': exc.detail, 'path': str(request.url.path)})
@app_v4.get('/teapot')
def teapot():
raise HTTPException(status_code=418, detail="I'm a teapot")
client_v4 = TestClient(app_v4)
response = client_v4.get('/teapot')
print(response.status_code, response.json())
Part 6: A Family of Related Custom Exceptions#
app_v5 = FastAPI()
class AppError(Exception):
def __init__(self, message: str):
self.message = message
class OutOfStockError(AppError):
pass
class InvalidCouponError(AppError):
pass
@app_v5.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError):
return JSONResponse(status_code=400, content={'error': type(exc).__name__, 'message': exc.message})
@app_v5.get('/checkout/{scenario}')
def checkout(scenario: str):
if scenario == 'stock':
raise OutOfStockError('this item is out of stock')
if scenario == 'coupon':
raise InvalidCouponError('this coupon has expired')
return {'status': 'ok'}
client_v5 = TestClient(app_v5)
print(client_v5.get('/checkout/stock').json())
print(client_v5.get('/checkout/coupon').json())
print(client_v5.get('/checkout/fine').json())
Part 7: A Catch-All Handler for Unexpected Exceptions#
app_v6 = FastAPI()
@app_v6.exception_handler(Exception)
async def catch_all_handler(request: Request, exc: Exception):
return JSONResponse(status_code=500, content={'error': 'internal_server_error'})
@app_v6.get('/crash')
def crash():
result = 1 / 0
return {'result': result}
client_v6 = TestClient(app_v6, raise_server_exceptions=False)
response = client_v6.get('/crash')
print(response.status_code, response.json())
Part 8: HTTPException headers - for Things Like WWW-Authenticate#
@app.get('/secure-resource')
def secure_resource():
raise HTTPException(
status_code=401,
detail='not authenticated',
headers={'WWW-Authenticate': 'Bearer'},
)
response = client.get('/secure-resource')
print(response.status_code)
print(response.headers['www-authenticate'])
Part 9: A Consistent Error Response Shape Across the App#
app_v7 = FastAPI()
def error_response(status_code: int, code: str, message: str):
return JSONResponse(status_code=status_code, content={'error': {'code': code, 'message': message}})
class PaymentDeclinedError(Exception):
pass
@app_v7.exception_handler(PaymentDeclinedError)
async def payment_declined_handler(request: Request, exc: PaymentDeclinedError):
return error_response(402, 'payment_declined', 'the payment method was declined')
@app_v7.get('/pay')
def pay():
raise PaymentDeclinedError()
client_v7 = TestClient(app_v7)
response = client_v7.get('/pay')
print(response.status_code, response.json())
Part 10: A Real Pattern - a Structured Global Error Handling Layer#
store_app = FastAPI()
def api_error(status_code: int, code: str, message: str):
return JSONResponse(status_code=status_code, content={'error': {'code': code, 'message': message}})
class StoreError(Exception):
def __init__(self, message: str):
self.message = message
class ProductNotFoundError(StoreError):
pass
class InsufficientStockError(StoreError):
pass
@store_app.exception_handler(ProductNotFoundError)
async def product_not_found_handler(request: Request, exc: ProductNotFoundError):
return api_error(404, 'product_not_found', exc.message)
@store_app.exception_handler(InsufficientStockError)
async def insufficient_stock_handler(request: Request, exc: InsufficientStockError):
return api_error(409, 'insufficient_stock', exc.message)
STOCK = {'MUG-1': 0, 'PLATE-2': 15}
@store_app.post('/orders/{sku}')
def place_order(sku: str):
if sku not in STOCK:
raise ProductNotFoundError(f'no such product: {sku}')
if STOCK[sku] <= 0:
raise InsufficientStockError(f'{sku} is out of stock')
return {'sku': sku, 'status': 'ordered'}
store_client = TestClient(store_app)
print(store_client.post('/orders/PLATE-2').json())
print(store_client.post('/orders/MUG-1').json())
print(store_client.post('/orders/UNKNOWN').json())
Wrap-Up: What You Learned#
- HTTPException remains the quickest way to fail a request with a specific status and detail.
- A custom exception class can carry whatever data a handler later needs.
- app.exception_handler registers a function that builds the response for a specific exception type.
- Overriding RequestValidationError's handler replaces the default 422 shape app-wide.
- Overriding HTTPException's own handler replaces the default error shape app-wide too.
- A handler registered for a shared base class also catches every subclass raised anywhere.
- A handler for the plain Exception class catches anything unexpected, avoiding a raw traceback.
- HTTPException accepts a headers argument, useful for things like WWW-Authenticate.
- A shared helper function keeps every error response the same consistent, nested shape.
- That wraps up error handling. Next up: Authentication Basics.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



