How do you debug and introspect async code?
Answer
Breakpoints and stepping
- Use your IDE’s debugger (e.g. VS Code, PyCharm) with breakpoints. With
asyncio, set breakpoints insideasync deffunctions; the debugger will stop when the event loop runs that coroutine. Be aware that stepping overawaitcan jump to other tasks—step carefully or use “run to cursor” to control flow.
asyncio.debug and slow callback detection
- Run with
PYTHONASYNCIODEBUG=1orasyncio.run(..., debug=True)to enable asyncio debug mode. This adds slow callback detection (warns if a callback blocks the loop too long) and better exception reporting. Helpful for finding blocking calls or long-running synchronous code in async paths.
Logging and tracing
- Add logging (especially around
awaitpoints and task creation) with correlation IDs or task names so you can trace a request across tasks. Useasyncio.current_task()ortask.get_name()to identify which task is running. Structured logging (JSON) with timestamps helps replay async flows.
Exception groups (Python 3.11+)
asyncio.gather(..., return_exceptions=False)and similar APIs can raiseExceptionGroupwhen multiple tasks fail. Useexcept*to catch and handle them:except* TimeoutError: ...or iterateeg.exceptionsto inspect each failure. Log the full group so you don’t lose secondary errors.
Introspection: inspect running tasks
- Use
asyncio.all_tasks()to see all tasks in the event loop; filter by name or coroutine if needed. Usetask.get_stack()ortask.get_coro()to inspect a task’s state. Useinspect.iscoroutine()/inspect.iscoroutinefunction()to check if something is a coroutine before awaiting.
Avoid blocking the event loop
- If you block in an async function (e.g.
time.sleep, CPU-heavy work, sync I/O), the whole loop stalls. Useawait asyncio.sleep()instead oftime.sleep(), run CPU work inrun_in_executor(), and use async I/O (e.g.aiohttp,aiosqlite). Debugger stepping can also “block” briefly—acceptable for debugging, but avoid long pauses in production paths.
Interview angle
- “How do you debug async code?” -
asynciodebug mode surfaces slow callbacks and un-awaited coroutines;asyncio.all_tasks()shows what’s currently scheduled; and per-task naming makes those dumps readable. A stuck service usually means a task blocked on something that never completes. - “How do you find a blocked event loop?” - debug mode logs callbacks exceeding a threshold. The external symptom is p99 latency rising on every endpoint at once, including ones doing nothing.
- “Why do exceptions sometimes vanish in async code?” - a task nobody awaits swallows its exception until garbage collection. Keep a reference and attach a done-callback, or own the task in a TaskGroup.