getsentry / getsentry/sentry-python
Async Stack Traces in Python
- Dominant language
- Python
- Stars
- 2.2k
- Forks
- 669
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 213
Description
# Async Stack Traces
Async Stack Traces are a problem in [many languages](https://www.draconianoverlord.com/2025/04/17/fixing-async-stack-traces.html/), 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](https://peps.python.org/pep-3156/#exceptions), as they are passed through the different await calls. This is illustrated by the following example:
```python
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
> 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:
```python
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: 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](https://docs.python.org/3/library/asyncio-dev.html) 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](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.set_exception_handler) can be registered or a [done callback](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.set_exception_handler) 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](https://discuss.python.org/t/asyncio-tasks-and-exception-handling-recommended-idioms/23806/2) as it can mess with program control flow and there seems to be an intention to move more towards [structured concurrency](https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/) for async applications, which is also supported by newer Python additions such as the [TaskGroup](https://docs.python.org/3/library/asyncio-task.html#task-groups), which was introduced in Python 3.11 and is receiving [further improvements](https://peps.python.org/pep-0789/) 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
Contributor guide
Assessment
This issue has not been assessed yet.