Introduction
Async bugs are notoriously hard to reproduce and diagnose. A forgotten await silently discards work, a blocking call freezes everything, and fire-and-forget tasks vanish without a trace. This lesson equips you with asyncio's debug mode, common mistake patterns and their fixes, introspection tools, and the new Python 3.14 CLI introspection commands.
Key Concepts
- Debug mode: A special asyncio mode that detects common mistakes like unawaited coroutines and slow callbacks.
- Introspection: The ability to inspect running tasks, their names, and their call stacks at runtime.
- Call graph: A representation of which tasks spawned which other tasks, useful for understanding complex async programs.
Real World Context
Your async web service has an intermittent bug: one in a hundred requests returns stale data. After enabling asyncio debug mode, you discover a coroutine that was never awaited — it was supposed to refresh a cache but silently created a coroutine object that was immediately garbage-collected. Python 3.14's asyncio ps command lets you inspect the running tasks of your production process without attaching a debugger.
Deep Dive
Debug Mode
pythonimport asyncio # Enable debug mode asyncio.run(main(), debug=True) # Or via environment variable # PYTHONASYNCIODEBUG=1 python script.py
Debug mode provides:
- Warnings for coroutines that were never awaited
- Slow callback detection
- Better error messages
Common Mistakes
1. Forgetting to await
python# WRONG async def main(): fetch_data() # Coroutine created but never runs! # RIGHT async def main(): await fetch_data()
2. Blocking the event loop
python# WRONG async def main(): time.sleep(5) # Blocks entire loop # RIGHT async def main(): await asyncio.sleep(5)
3. Creating tasks without awaiting
python# Tasks may not complete! async def main(): asyncio.create_task(background_work()) # main() ends, task may be cancelled # Better: track and await tasks async def main(): task = asyncio.create_task(background_work()) # ... do other work ... await task # Ensure completion
Introspection
python# Get all running tasks for task in asyncio.all_tasks(): print(task.get_name(), task.get_coro()) # Current running task current = asyncio.current_task()
Asyncio CLI Introspection (Python 3.14+)
Python 3.14 adds command-line tools to inspect running asyncio programs:
bash# Show all running tasks in a process python -m asyncio ps <PID> # Show task call tree python -m asyncio pstree <PID>
You can also capture call graphs programmatically:
pythonimport asyncio async def main(): # Capture the current call graph graph = asyncio.capture_call_graph() asyncio.print_call_graph(graph)
Common Pitfalls
- Forgetting to await a coroutine — The coroutine object is created but never executed. The function appears to do nothing and Python emits a
RuntimeWarningthat is easy to miss. - Catching Exception too broadly — Catching
Exceptioninside a coroutine will swallowCancelledErrorin Python < 3.11, preventing proper task cancellation. Always re-raiseCancelledError. - Not using debug mode during development — Many async bugs are silent. Debug mode catches unawaited coroutines, slow callbacks, and other issues that would otherwise go undetected.
Best Practices
- Enable debug mode in development and CI — Set
PYTHONASYNCIODEBUG=1in your development environment to catch issues early. - Name your tasks — Use
asyncio.create_task(coro(), name="refresh-cache")so that introspection output is readable and you can identify tasks in logs. - Use Python 3.14 CLI tools for production debugging —
python -m asyncio ps <PID>andpstreelet you inspect running tasks without modifying code or attaching a debugger.
Summary
- Enable debug mode with
asyncio.run(main(), debug=True)orPYTHONASYNCIODEBUG=1to detect common async mistakes. - The three most common async bugs are: forgetting to await, blocking the event loop, and fire-and-forget tasks.
- Use
asyncio.all_tasks()andasyncio.current_task()for runtime introspection. - Python 3.14 adds
asyncio ps,pstree, andcapture_call_graph()for inspecting async programs. - Always name your tasks for easier debugging.
Code Examples
import asyncio
import warnings
# Catch 'coroutine was never awaited' warnings
warnings.filterwarnings('error', category=RuntimeWarning)
# Custom task factory for debugging
async def main():
loop = asyncio.get_running_loop()
def task_factory(loop, coro, **kwargs):
task = asyncio.Task(coro, **kwargs)
print(f"Created task: {task.get_name()}")
return task
loop.set_task_factory(task_factory)
await some_work()
asyncio.run(main())