为何在done回调中调用future.result()会改变异常回溯信息?
Asyncio协程异常回溯被截断的问题分析与解决
问题现象
当同时满足以下三个条件时,CPython环境下asyncio协程抛出的异常回溯会丢失内部调用栈:
- 给协程对应的Future对象绑定了调用
fut.result()的done回调 - 在其他代码位置
await该Future - 协程内部抛出异常
移除done回调里的fut.result()调用后,就能得到包含协程内部调用栈的完整回溯;而在PyPy环境中,无论是否调用fut.result(),都能正常显示完整回溯。
复现代码
import asyncio async def my_traceback_gets_mangled(): def bar(): raise TypeError('I wish I had a traceback that let me find this bogus raise') await asyncio.sleep(1) bar() def done_cb(fut): try: fut.result() except: pass async def main(): fut = asyncio.ensure_future(my_traceback_gets_mangled()) fut.add_done_callback(done_cb) await fut asyncio.run(main())
输出对比
调用fut.result()时的截断回溯
Traceback (most recent call last): File "/tmp/foo.py", line 18, in <module> asyncio.run(main()) File "/usr/lib/python3.10/asyncio/runners.py", line 44, in run return loop.run_until_complete(main) File "/usr/lib/python3.10/asyncio/base_events.py", line 649, in run_until_complete return future.result() File "/tmp/foo.py", line 16, in main await fut TypeError: I wish I had a traceback that let me find this bogus raise
移除fut.result()后的完整回溯
Traceback (most recent call last): File "/tmp/foo.py", line 18, in <module> asyncio.run(main()) File "/usr/lib/python3.10/asyncio/runners.py", line 44, in run return loop.run_until_complete(main) File "/usr/lib/python3.10/asyncio/base_events.py", line 649, in run_until_complete return future.result() File "/tmp/foo.py", line 16, in main await fut File "/tmp/foo.py", line 12, in my_traceback_gets_mangled bar() File "/tmp/foo.py", line 10, in bar raise TypeError('I wish I had a traceback that let me find this bogus raise') TypeError: I wish I had a traceback that let me find this bogus raise
原因
这是CPython中asyncio Future的实现细节:第一次调用fut.result()时,会修改异常对象的回溯信息,只保留到result()调用点的栈轨迹。后续通过await触发异常抛出时,会复用这个已经被修改的异常对象,导致协程内部的原始调用栈丢失。
PyPy的asyncio实现没有这个回溯截断逻辑,所以能保留完整调用栈。
解决办法
1. 取消done回调中的fut.result()调用
如果done回调不需要处理异常,直接删掉fut.result()相关代码即可,这样await时会抛出带完整回溯的异常。
2. 用fut.exception()替代fut.result()获取异常
如果需要在done回调中处理异常,使用fut.exception()获取异常对象,避免触发result()的回溯修改逻辑:
def done_cb(fut): exc = fut.exception() if exc is not None: # 在这里处理异常,比如打印日志 print(f"捕获异常: {exc}")
3. 手动保留并恢复原始回溯
如果必须调用fut.result(),可以先保存原始异常的回溯信息,之后再恢复:
def done_cb(fut): exc = fut.exception() original_traceback = exc.__traceback__ if exc else None try: fut.result() except: pass # 恢复原始回溯,确保后续await能拿到完整栈 if exc: exc.__traceback__ = original_traceback
内容的提问来源于stack exchange,提问作者Sam Hartman
相关产品推荐
相关产品推荐

