Mathew K Analytics

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…

⬇ Download notebookOpen in Colab ↗

📓 Full notebook

Download .ipynb

FastAPI 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())
{'id': 1, 'name': 'Mug'}
{'detail': 'item not found'}

Part 2: A Custom Exception Class#

class ItemNotFoundError(Exception):
    def __init__(self, item_id: int):
        self.item_id = item_id
print(issubclass(ItemNotFoundError, Exception))
True

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())
{'id': 1, 'name': 'Mug'}
{'detail': 'item 99 not found'}

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())
{'error': 'validation_failed', 'field_count': 1}

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())
418 {'error': "I'm a teapot", 'path': '/teapot'}

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())
{'error': 'OutOfStockError', 'message': 'this item is out of stock'}
{'error': 'InvalidCouponError', 'message': 'this coupon has expired'}
{'status': 'ok'}

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())
500 {'error': 'internal_server_error'}

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'])
401
Bearer

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())
402 {'error': {'code': 'payment_declined', 'message': 'the payment method was declined'}}

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())
{'sku': 'PLATE-2', 'status': 'ordered'}
{'error': {'code': 'insufficient_stock', 'message': 'MUG-1 is out of stock'}}
{'error': {'code': 'product_not_found', 'message': 'no such product: UNKNOWN'}}

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.