getsentry / getsentry/sentry-python

Async Stack Traces in Python

未关闭
#4,693 1 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看
Better Async Support Improvement Python Traces
主要语言
Python
星标
2.2k
派生
669
平均合并
1 天 1 小时
30 天内合并 PR
213

描述

# 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

贡献指南

打开贡献指南

评估

这个 Issue 还没有评估数据。

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。