Skip to content

Async Stack Traces in Python #4693

@srothh

Description

@srothh

Async Stack Traces

Async Stack Traces are a problem in many languages, as often the calling frames are gone by the time an async task/ Promise finishes or exceptions get lost entirely. This issue details the behaviour of async stack traces in Python and discusses open issues.

Behaviour in Python

In Python, when async tasks/coroutines are properly awaited, stack traces and exceptions usually get logged properly, as they are passed through the different await calls. This is illustrated by the following example:

import asyncio

def main():
    asyncio.run(foo())

async def foo():
    await asyncio.sleep(0.100)
    await bar()

async def bar():
    await asyncio.sleep(0.200)
    await blow_up()

async def blow_up():
    await asyncio.sleep(0.300)
    raise RuntimeError("nested async panic!")

if __name__ == "__main__":
    main()

which outputs a proper stacktrace like this:

> Traceback (most recent call last):
>   File "/Users/simon/code/sentry-python/sentry_sdk/stacktracetest.py", line 19, in <module>
>     main()
>     ~~~~^^
>   File "/Users/simon/code/sentry-python/sentry_sdk/stacktracetest.py", line 4, in main
>     asyncio.run(foo())
>     ~~~~~~~~~~~^^^^^^^
>   File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/runners.py", line 195, in run
>     return runner.run(main)
>            ~~~~~~~~~~^^^^^^
>   File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/runners.py", line 118, in run
>     return self._loop.run_until_complete(task)
>            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
>   File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py", line 725, in run_until_complete
>     return future.result()
>            ~~~~~~~~~~~~~^^
>   File "/Users/simon/code/sentry-python/sentry_sdk/stacktracetest.py", line 8, in foo
>     await bar()
>   File "/Users/simon/code/sentry-python/sentry_sdk/stacktracetest.py", line 12, in bar
>     await blow_up()
>   File "/Users/simon/code/sentry-python/sentry_sdk/stacktracetest.py", line 16, in blow_up
>     raise RuntimeError("nested async panic!")
> RuntimeError: nested async panic!

This makes asynchronous stacktraces in Python much less of a problem than e.g. Javascript.

However, when tasks run detached (are created, but not awaited), the calling frames may disappear and the exception will get lost. The following example:

import asyncio

async def boom():
    await asyncio.sleep(0.01)
    raise RuntimeError("detached task blew up")

async def caller_detached():
    asyncio.create_task(boom())         
    await asyncio.sleep(0.02)         

asyncio.run(caller_detaches())

results in this stacktrace:

Task exception was never retrieved
future: <Task finished name='Task-2' coro=<boom() done, defined at /Users/simon/code/sentry-python/sentry_sdk/stacktracetest.py:3> exception=RuntimeError('detached task blew up')>
Traceback (most recent call last):
  File "/Users/simon/code/sentry-python/sentry_sdk/stacktracetest.py", line 5, in boom
    raise RuntimeError("detached task blew up")
RuntimeError: detached task blew up

Notice that the calling task caller_detached does not show up in the stacktrace, and an error "Task exception was never retrieved" gets output. To get the stacktrace including the calling source, debug mode can be enabled or a task factory can be set that snapshots the current stacktrace before a task is launched. Both of these solutions however come with a big overhead, as they create a snapshot of the stacktrace in every case, not just when there is an exception. To log the exception, either a custom exception handler can be registered or a done callback wrapping task.result() in try/except can be attached.

Of note however is that this behaviour of simply creating tasks seems to be discouraged by core python developers as it can mess with program control flow and there seems to be an intention to move more towards structured concurrency for async applications, which is also supported by newer Python additions such as the TaskGroup, which was introduced in Python 3.11 and is receiving further improvements to better fit asyncio in Python 3.14.

Sources and interesting reads:
https://www.draconianoverlord.com/2025/04/17/fixing-async-stack-traces.html/
https://peps.python.org/pep-3156/#exceptions
https://docs.python.org/3/library/asyncio-dev.html
https://docs.python.org/3/library/asyncio-eventloop.html
https://discuss.python.org/t/asyncio-tasks-and-exception-handling-recommended-idioms/23806
https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/
https://peps.python.org/pep-0789/
https://docs.python.org/3/library/asyncio-task.html

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions