Blog
Observability2026-W183 min readby delve

Sentry and FastAPI Custom Exception Handlers — The Silent Swallow

FastAPI's Sentry integration captures unhandled exceptions automatically — but the moment you register an `@app.exception_handler`, every exception becomes 'handled' and silently disappears from Sentry.

A dark server room with a single computer monitor showing an empty log viewer while a red LED alarm glows on a nearby rack.

The problem

FastAPI's Sentry integration (via sentry_sdk.integrations.starlette.StarletteIntegration and sentry_sdk.integrations.fastapi.FastApiIntegration) automatically captures unhandled exceptions and creates transactions per request. The operative word is unhandled. The moment you register a custom exception handler with @app.exception_handler(Exception), Starlette routes all exceptions through it. Sentry's integration hooks never see them. You get a clean HTTP 500 back to the client and silence in Sentry.

The approach

The fix is one line in the custom handler:

@app.exception_handler(Exception) async def unhandled_exception_handler(request: Request, exc: Exception): logger.exception("Unhandled exception: %s", exc) sentry_sdk.capture_exception(exc) # ← this line was missing return JSONResponse(status_code=500, content={"detail": "Internal server error"})

The subtlety is that sentry_sdk.capture_exception() is synchronous and safe to call from async context — it buffers to a thread-local queue and flushes on a background thread. It doesn't block the event loop.

For the hot-path ingest endpoint, a Sentry span makes sense for latency tracking without adding per-exception noise:

with sentry_sdk.start_span(op="ingest", description=f"ingest {len(events)} events"): sentry_sdk.set_tag("org_id", str(org_id)) # ... do the work sentry_sdk.add_breadcrumb(message="ingest_events", data={"accepted": inserted}, level="info")

The add_breadcrumb call adds context to the next error capture in the same request — not an event on its own — so it's effectively free from a quota perspective.

What I learned

The Starlette/FastAPI Sentry integration is a middleware that intercepts exceptions before your handlers see them — but only if your handlers don't swallow them first. Any @app.exception_handler that returns a response instead of re-raising causes the exception to exit the Sentry middleware's scope without being captured. The fix is always explicit: capture_exception(exc) in the handler body. Relying on the integration to do it automatically is correct for unhandled cases, not for handled ones.