ExceptionGroup本质是继承BaseException的异常树,不可变,需用except*解包子异常;asyncio.gather需设return_exceptions=True才能生成它;add_note()仅作用于外层。

ExceptionGroup 本质是异常树,不是列表或元组
它继承自 BaseException,所以能被 raise 和 except 捕获;但它内部用 exceptions 属性存一个只读元组,每个元素都是真实异常(包括嵌套的 ExceptionGroup)。这决定了它不是“多个异常拼一起”,而是“一个异常描述一组并发失败”——语义上就和 ValueError 平级,但结构上可展开。
常见错误:把 ExceptionGroup("msg", [e1, e2]) 当作普通容器去 .append() 或索引赋值,会报 TypeError;它不可变,修改子异常必须遍历 eg.exceptions 单独操作。
except* 是唯一能解包子异常的语法
except ValueError: 永远匹配不到 ExceptionGroup 里的 ValueError 子项,因为类型不继承。只有 except* ValueError: 才会对组内每个子异常单独做 isinstance() 判断,并把匹配上的打包成新 ExceptionGroup 绑定到变量。
- 一个
except*块可匹配零个、一个或多个子异常,不中断其余未匹配项 - 多个
except*块按顺序执行,未被前一个捕获的子异常会留给后一个继续筛 - 没被任何
except*匹配的子异常,最终仍以原ExceptionGroup形式冒泡
asyncio.gather 默认不产生 ExceptionGroup
关键点在 return_exceptions 参数:asyncio.gather(..., return_exceptions=False)(默认)遇到第一个异常就停,根本不会聚合;必须显式设为 True,让所有任务结果(含异常)返回为列表,再手动挑出异常构造 ExceptionGroup。
立即学习“Python免费学习笔记(深入)”;
示例:
results = await asyncio.gather(task_a(), task_b(), return_exceptions=True)
exceptions = [r for r in results if isinstance(r, BaseException)]
if exceptions:
raise ExceptionGroup("tasks failed", exceptions)漏掉 return_exceptions=True 或忘了过滤 BaseException 实例,就会收不到预期的多异常流。
add_note() 不作用于子异常,容易误以为全局生效
ExceptionGroup 自身有 add_note(),但它只修改外层消息,不影响 eg.exceptions 里的子异常。想给某个 ValueError 加备注,得先遍历:
for exc in eg.exceptions:
if isinstance(exc, ValueError):
exc.add_note("user input was empty")直接写 eg.add_note("all tasks failed") 没错,但别指望它让子异常的 traceback 多出这行——子异常的上下文还是各自独立的。
最常被忽略的是:ExceptionGroup 的存在本身就意味着你正在处理并发失败场景,而不是单次调用的多类型错误;如果没用 asyncio、concurrent.futures 或手动聚合,就根本不会触发它——别试图用 except* 去捕获同步代码里抛出的普通异常。


















