Why FastAPI Debugging Is Different#
FastAPI runs on ASGI (async I/O). That means two things for debugging: errors happen in async context, and standard print-debugging often misses
the timing of when things fail. A validation error before your function runs looks identical in the terminal to a runtime error inside your
function — until you know where to look.
This guide covers the full debugging workflow: reading FastAPI's built-in error output, setting up VS Code breakpoints for async code, and using
AI tools for cross-file root cause analysis.
Step 1: Read the Terminal Output First#
When FastAPI starts with uvicorn app.main:app --reload, every request and every error prints to the terminal. Don't jump to the code before
reading it.
A 422 validation error looks like:
INFO: 127.0.0.1:54234 - "POST /users HTTP/1.1" 422 Unprocessable Entity
A 500 server error looks like:
INFO: 127.0.0.1:54234 - "POST /users HTTP/1.1" 500 Internal Server Error
ERROR: Exception in ASGI application
Traceback (most recent call last):
File "/app/routers/users.py", line 34, in create_user
result = await db.insert(user.dict())
AttributeError: 'NoneType' object has no attribute 'insert'
The status code tells you where the error originated:
| Status | Origin | Where to look |
|---|
| 422 | Pydantic validation | Request body vs model definition |
| 401/403 | Auth middleware | Dependency functions, token validation |
| 500 | Your route function | Traceback in terminal |
| 502/504 | Upstream service | External API calls, DB connection |
Step 2: Use /docs for Request Validation#
FastAPI auto-generates interactive docs at http://localhost:8000/docs. Before writing any debug code, test your endpoint directly in Swagger UI.
Tip: Swagger UI is generated from your actual Pydantic models — it shows exactly what FastAPI expects. If your request body matches the
Swagger schema and still fails, the bug is inside your route function, not in the request.
For 422 errors specifically: the /docs schema shows required vs optional fields, expected types, and field constraints. Compare your actual
request against it directly.
Step 3: Add Structured Logging#
print() works for simple cases. For async FastAPI, structured logging gives you context you can't get from print:
import logging
import structlog
logger = structlog.get_logger()
@app.post("/orders")
async def create_order(order: CreateOrder, user_id: str = Depends(get_current_user)):
logger.info("create_order.start", user_id=user_id, item_id=order.item_id)
try:
result = await order_service.create(order, user_id)
logger.info("create_order.success", order_id=result.id)
return result
except Exception as e:
logger.error("create_order.failed", error=str(e), user_id=user_id)
raise
structlog adds key-value pairs to every log line. When you grep the logs for a specific user or order, you see the full sequence of events — not
just where it crashed.
Step 4: Set Up VS Code Debugger for FastAPI
VS Code's Python debugger works with FastAPI but needs a launch configuration that handles uvicorn correctly.
Create .vscode/launch.json in your project root:
{
"version": "0.2.0",
"configurations": [
{
"name": "FastAPI Debug",
"type": "debugpy",
"request": "launch",
"module": "uvicorn",
"args": [
"app.main:app",
"--reload",
"--port",
"8000"
],
"jinja": true,
"justMyCode": true
}
]
}
▎ Note: Use "module": "uvicorn" not "program": "uvicorn". The module form handles Python path resolution correctly across virtual environments and
▎ makes breakpoints in your code work reliably.
Now set a breakpoint: click the gutter (left of line numbers) on any line inside a route function. Press F5 to start debugging. Send a request —
execution pauses at your breakpoint and you can inspect all variables.
Step 5: Debug Async Functions
Async await points are common places for bugs — the awaited coroutine can raise, return None, or hang. Set breakpoints ON the await line to
inspect what happens:
@app.get("/users/{user_id}")
async def get_user(user_id: str):
user = await db.find_user(user_id) # ← Set breakpoint here
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
When the debugger pauses at that line, F10 (step over) executes the await and moves to the next line. The user variable in the debug panel shows
what db.find_user() actually returned — including None if the record doesn't exist.
▎ Warning: Don't set breakpoints inside async generator functions or inside asyncio.gather() calls. The debugger can lose track of coroutine
▎ context in those cases. Wrap the suspicious code in a regular async function and debug that instead.
Step 6: Debug Dependencies
FastAPI's dependency injection runs before your route function. Bugs in dependencies show stack traces that point into FastAPI internals, not your
code — which is confusing.
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(status_code=401)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
user_id = payload.get("sub")
except JWTError:
raise credentials_exception
return await db.get_user(user_id)
Set a breakpoint inside the dependency function itself. When a request hits an endpoint that uses this dependency, the debugger pauses in the
dependency — letting you inspect the token, payload, and user_id before the route function runs.
Step 7: Reproduce Production Errors Locally
Production FastAPI errors often include a traceback in your logs (if you have log aggregation). To reproduce:
1. Copy the exact request body from your logs
2. Paste it into /docs → "Try it out" → send
3. If it fails locally, you've reproduced it
For errors that only happen with specific DB state:
# Add a temporary test endpoint — remove after debugging
@app.get("/debug/user/{user_id}", include_in_schema=False)
async def debug_user(user_id: str):
user = await db.find_user(user_id)
order_count = await order_service.count_for_user(user_id)
return {"user": user, "order_count": order_count, "has_team": user.team_id is not None}
include_in_schema=False hides it from Swagger. Gives you a debugging endpoint without polluting your API surface.
Step 8: Use AI for Cross-File Errors
FastAPI apps span multiple files — routes, services, models, dependencies, database layer. When a traceback points deep into your service layer,
the root cause is often 3-4 files away from where the error surfaces.
▎ Error: AttributeError: 'NoneType' object has no attribute 'stripe_customer_id' in billing_service.py line 47
Manual approach: read the traceback, find what called billing_service, find what passed the user object, find where user was loaded, find where it
might return None.
DebugAI approach: highlight the error → click Debug with AI. It reads the traceback, loads billing_service.py, loads the caller, loads the user
model and query — and tells you that get_user() in users.py returns None for users who registered before the stripe_customer_id column was added
(schema migration was not backfilled).
Cross-file bugs in async services are the fastest DebugAI ROI — the dependency chain is too long to trace manually in a reasonable time.
Common FastAPI Errors Quick Reference
┌─────────────────────────────────────┬─────────────────────────────────────────┬─────────────────────────────────┐
│ Error │ Likely cause │ First thing to check │
├─────────────────────────────────────┼─────────────────────────────────────────┼─────────────────────────────────┤
│ 422 Unprocessable Entity │ Pydantic validation fail │ detail array in response body │
├─────────────────────────────────────┼─────────────────────────────────────────┼─────────────────────────────────┤
│ 500 Internal Server Error │ Exception in route/dependency │ Terminal traceback │
├─────────────────────────────────────┼─────────────────────────────────────────┼─────────────────────────────────┤
│ 401 Unauthorized │ Auth dependency failed │ JWT decode, token expiry │
├─────────────────────────────────────┼─────────────────────────────────────────┼─────────────────────────────────┤
│ 404 Not Found │ Wrong route or raise HTTPException(404) │ Route path, HTTP method │
├─────────────────────────────────────┼─────────────────────────────────────────┼─────────────────────────────────┤
│ RuntimeError: no running event loop │ Sync code calling async │ Ensure await on async functions │
├─────────────────────────────────────┼─────────────────────────────────────────┼─────────────────────────────────┤
│ greenlet_spawn has not been called │ SQLAlchemy async session misuse │ Use AsyncSession, not Session │
└─────────────────────────────────────┴─────────────────────────────────────────┴─────────────────────────────────┘
FastAPI + VS Code debugger covers 90% of bugs. For the other 10% — cross-service async timing issues and production-only DB state bugs — AI
analysis with codebase context gets you there without hours of log-reading.