Python Async Debugging — Core Concepts
Why Async Code Is Harder to Debug
Traditional debugging tools assume linear execution — one thing happens after another. Async code breaks that assumption. Multiple coroutines interleave, stack traces show the event loop instead of your code, and errors can vanish into unobserved tasks.
Debug Mode: Your First Tool
Enable it with any of these:
# Option 1: In code
asyncio.run(main(), debug=True)
# Option 2: Environment variable
# PYTHONASYNCIODEBUG=1 python app.py
# Option 3: At runtime
loop = asyncio.get_event_loop()
loop.set_debug(True)
Debug mode enables three key behaviors:
- Slow callback warnings — logs when a callback takes longer than 100ms
- Coroutine creation tracking — records where each coroutine was created
- Never-awaited detection — warns about coroutines that were made but never driven
Inspecting Running Tasks
Python 3.7+ gives you asyncio.all_tasks() to see every active task:
async def debug_snapshot():
for task in asyncio.all_tasks():
print(f"{task.get_name()}: {task.get_coro()}")
if not task.done():
task.print_stack() # Print the coroutine's stack trace
This is invaluable when your program hangs — you can see exactly which tasks are alive and where they’re suspended.
Common Bug: Unobserved Exceptions
When a task raises an exception but nobody awaits the task, the error is silently lost:
async def broken():
raise ValueError("oops")
async def main():
asyncio.create_task(broken()) # Exception vanishes!
await asyncio.sleep(10)
Fix this by always awaiting tasks or using a custom exception handler:
def handle_exception(loop, context):
exception = context.get("exception")
message = context.get("message")
print(f"Unhandled: {message} - {exception}")
loop = asyncio.get_event_loop()
loop.set_exception_handler(handle_exception)
Slow Callback Detection
A callback or coroutine step that runs too long blocks the entire event loop. Debug mode logs a warning:
Executing <Task name='Task-3'> took 0.250 seconds
You can tune the threshold:
loop.slow_callback_duration = 0.05 # Warn at 50ms instead of 100ms
The Stack Trace Problem
Normal exceptions in async code show the event loop’s internals instead of your code:
Task exception was never retrieved
File "asyncio/tasks.py", line 232, in __step
Debug mode fixes this by recording the creation site of each task, so the traceback includes where you created the task, not just where it failed.
Deadlock Detection
Async deadlocks happen when tasks wait for each other in a cycle. There’s no built-in detector, but you can build one:
async def detect_stuck_tasks(threshold_seconds=30):
"""Log tasks that haven't progressed."""
while True:
await asyncio.sleep(threshold_seconds)
for task in asyncio.all_tasks():
if not task.done():
print(f"Still running: {task.get_name()}")
task.print_stack(limit=5)
Common Misconception: “print() Is Enough for Async Debugging”
Print statements are unreliable in async code because output from different tasks interleaves unpredictably. Use structured logging with task identifiers:
import logging
async def worker(name):
logger = logging.getLogger(name)
logger.info("Starting work")
await do_stuff()
logger.info("Done")
Quick Debugging Checklist
- Program hangs? → Use
asyncio.all_tasks()andtask.print_stack()to find stuck tasks - Silent failures? → Set a custom exception handler on the loop
- Mystery slowdowns? → Enable debug mode and check slow callback warnings
- “Coroutine was never awaited”? → You called an async function without
awaitorcreate_task() - Wrong results? → Check for shared mutable state between concurrent tasks
One thing to remember: asyncio.run(main(), debug=True) is the async equivalent of turning on the lights — it reveals slow callbacks, forgotten coroutines, and lost exceptions that are invisible by default.
See Also
- Python Actor Model Why treating each piece of your program like a person with their own mailbox makes concurrency way less scary.
- Python Aiocache Caching aiocache remembers expensive answers so your async Python app doesn't waste time asking the same question twice.
- Python Aiofiles Async Io aiofiles lets your async Python program read and write files without freezing — because normal file operations secretly block everything.
- Python Aiohttp Understand Aiohttp through an everyday analogy so Python behavior feels intuitive, not random.
- Python Anyio Portability AnyIO lets your async Python code work with any async library — write once, run on asyncio or Trio without changes.