Exception logging in FastAPI with request_id + structured context

When your FastAPI app crashes, you need to know which request caused it. Without proper context, exception logs become a pile of stack traces with no way to trace them back to specific user actions.

The Problem: Lost Context in Errors

Default FastAPI exception handling logs the stack trace but loses the request context:

from fastapi import FastAPI

app = FastAPI()

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    # This exception loses all request context
    raise ValueError(f"User {user_id} not found")

When this crashes, you see:

ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "...", line 42, in __call__
    ...
ValueError: User 123 not found

What’s missing:

  • Which request triggered this error?

  • What was the URL path?

  • Who made the request?

  • How do I correlate this with other logs from the same request?

The Solution: Automatic Context Preservation

fapilog’s middleware automatically preserves request context in error logs:

from fastapi import FastAPI
from fapilog.fastapi import FastAPIBuilder

app = FastAPI(
    lifespan=FastAPIBuilder()
        .with_preset("fastapi")
        .build()
)

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    raise ValueError(f"User {user_id} not found")

Now when the same exception occurs, fapilog logs:

{
  "timestamp": "2026-01-21T10:30:00.123Z",
  "level": "ERROR",
  "message": "request_failed",
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "path": "/users/123",
  "method": "GET",
  "status_code": 500,
  "error_type": "ValueError",
  "error": "User 123 not found",
  "latency_ms": 12.5
}

Every field you need to debug the issue is right there.

Custom Exception Handlers

For custom exception handling, use get_request_logger to maintain context:

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fapilog.fastapi import FastAPIBuilder, get_request_logger

app = FastAPI(
    lifespan=FastAPIBuilder()
        .with_preset("fastapi")
        .build()
)

class UserNotFoundError(Exception):
    def __init__(self, user_id: int):
        self.user_id = user_id
        super().__init__(f"User {user_id} not found")

@app.exception_handler(UserNotFoundError)
async def handle_user_not_found(request: Request, exc: UserNotFoundError):
    logger = await get_request_logger(request)
    await logger.warning(
        "user_not_found",
        user_id=exc.user_id,
        path=request.url.path,
    )
    return JSONResponse(
        status_code=404,
        content={"error": "User not found", "user_id": exc.user_id},
    )

The logger automatically includes the request_id from the current request context.

Structured Error Fields

When fapilog captures an exception, it includes these structured fields:

Field

Description

error_type

Exception class name (e.g., ValueError)

error

Exception message (str(exception))

request_id

Correlation ID for the request

path

Request URL path

method

HTTP method (GET, POST, etc.)

status_code

HTTP response status

latency_ms

Request duration in milliseconds

For full stack trace capture, pass exc_info=True:

try:
    process_payment(order)
except PaymentError as e:
    await logger.error(
        "payment_failed",
        order_id=order.id,
        exc_info=True,  # Captures full traceback
    )
    raise

This adds the exception field with detailed traceback info:

{
  "level": "ERROR",
  "message": "payment_failed",
  "request_id": "abc-123",
  "order_id": "order-456",
  "exception": {
    "error.type": "PaymentError",
    "error.message": "Card declined",
    "error.stack": "Traceback (most recent call last):\n  ...",
    "error.frames": [
      {"file": "payment.py", "line": 42, "function": "charge", "code": "..."}
    ]
  }
}

Going Deeper