<p>应直接捕获 ExceptionGroup 或用 except 解包子异常,因 Python 3.11+ 中 asyncio.gather 抛出的 ExceptionGroup 不是 Exception 子类,导致 except Exception: 无法捕获而静默降级;except 可并行匹配子异常,TaskGroup 则提供结构化并发与自动清理。</p>

直接捕获 ExceptionGroup 或用 except* 解包子异常,而不是写 except Exception:——后者在 Python 3.11+ 中对并发失败完全失效。
asyncio.gather 抛出 ExceptionGroup 却没进 except 块
这是最常见静默降级场景:asyncio.gather(..., return_exceptions=False) 在任一子任务出错时,不再抛单个异常,而是封装为 ExceptionGroup。而 except Exception: 根本不匹配它,因为 ExceptionGroup 继承自 BaseException,但**不是 Exception 的子类**。
- 现象:日志里看不到错误,协程静默退出,HTTP 返回 500 却无告警
- 验证方式:在 except 块里加
print(type(e)),你会看到<class 'ExceptionGroup'>,而非ValueError等具体类型 - 别试图用
isinstance(e, ValueError)判断顶层异常——永远为False - 修复不是改
gather参数,而是改异常处理逻辑
用 except* 正确解包子异常类型
except* 不是语法糖,它是唯一能直接匹配 ExceptionGroup 内部子异常的机制,且支持多分支并行触发。
- 写法必须是
except* ValueError as eg:,不是except ValueError as e: -
eg是一个子集ExceptionGroup,其eg.exceptions是匹配上的那些子异常列表 - 多个
except*可同时生效:比如一个ExceptionGroup含TimeoutError和ConnectionError,两个分支都会执行 - 未被任何
except*匹配的子异常,会继续向上抛出——所以宽泛类型如except* Exception:应放在最后
兼容旧版本 Python 的 fallback 方案
如果你的代码要跑在 Python 3.10 或更低版本,ExceptionGroup 类不存在,连 import 都会失败。
立即学习“Python免费学习笔记(深入)”;
- 不要写
from exceptiongroup import ExceptionGroup并假设它总存在 - 推荐用
try/except ImportError动态加载,或直接依赖第三方 backport 库:pip install exceptiongroup - 启动时注册统一
sys.excepthook是零侵入方案:它能在所有 Python 版本下打印完整ExceptionGroup结构,包括每个子异常的 traceback - 注意:backport 库的
ExceptionGroup与原生实现行为一致,但except*语法仍是 Python 3.11+ 专属,旧版本无法解析
TaskGroup 替代 gather 才能真正释放 ExceptionGroup 价值
asyncio.gather 只聚合异常,不管理生命周期;asyncio.TaskGroup 才提供结构化并发语义——异常发生时自动 cancel 其他任务,并保证 finally/cleanup 执行。
- 必须用
async with asyncio.TaskGroup() as tg:,不能在外部调用tg.create_task() -
tg.create_task()启动的任务,失败后会被自动收集进同一个ExceptionGroup,且已启动任务有机会完成清理 - 别在
TaskGroup块内直接await单个 task,这会破坏结构约束,引发RuntimeError: TaskGroup is closed - 结果顺序不确定,别依赖
[t1.result(), t2.result()]的索引对应原始调用顺序
真正容易被忽略的是:ExceptionGroup 不是“多个异常拼一起”,而是表达并发失败语义的容器;你不能靠 catch-all 式的 except Exception: 来兜底,必须显式选择是解包处理、还是透传、还是记录后重新 raise——每种选择都影响可观测性和故障恢复能力。


















