async/await异常不会自动冒泡,必须显式调度协程(如await、asyncio.run)或检查task(如task.exception())才能捕获;gather默认快速失败,需return_exceptions=true才聚合异常;未处理异常会在event loop关闭时转为runtimeerror输出到stderr。

async/await 中的异常不会自动冒泡到外层同步代码
你写了个 async def,里面 raise ValueError("boom"),但调用它时没加 await 或没用 asyncio.run(),结果异常根本没抛出来——不是没发生,是它卡在了 coroutine object 里,等着被驱动执行。Python 不会主动执行协程,也不会替你检查它内部有没有异常。
常见错误现象:RuntimeWarning: coroutine 'xxx' was never awaited,或者更隐蔽的:程序静默退出,日志里啥也没有。
- 必须显式调度协程:用
await(在另一个async函数内)、asyncio.run()(顶层入口)、或loop.run_until_complete() - 直接打印或返回协程对象(比如
print(fetch_data()))不会触发执行,自然也捕获不到异常 - 单元测试里如果忘了
await被测协程,断言会永远不运行,甚至可能误判为“通过”
Task 对象的异常要 await 才能触发
用 asyncio.create_task() 启动一个任务后,它的异常不会立刻向上抛,而是“挂起”在 Task 实例上,直到你对它做 await task 或调用 task.result()。
使用场景:并发发多个请求,其中一个失败,但你不希望整个流程被中断,而是等全部结束再统一处理错误。
立即学习“Python免费学习笔记(深入)”;
本文档主要讲述的是Android AsyncChannel源码分析;AsyncChannel类用于处理两个Handler之间的异步消息传递,消息传递的Handler可以出于同一进程,也可以处于不同进程,不同进程之间的Handler消息传递使用Android的Binder通信机制来实现。希望本文档会给有需要的朋友带来帮助;感兴趣的朋友可以过来看看
-
await task会立即抛出该任务内部的异常(如果有的话) -
task.exception()可以安全获取异常对象(返回None表示没异常),适合非阻塞检查 - 如果任务已取消,
task.exception()返回CancelledError;但await task会直接 raise 它,需注意是否要捕获 - 忘记 await 已创建的 task,可能导致异常丢失、资源未释放,且难以 debug
asyncio.gather() 的异常传播策略:默认“快速失败”
asyncio.gather() 默认只要有一个子协程出错,就立刻停止其余任务并抛出异常——这和你直觉中“等所有都跑完再汇总错误”不一样。
参数差异:return_exceptions=True 是关键开关,它让失败的子协程返回异常实例而非抛出。
- 默认行为(
return_exceptions=False):第一个异常中断整个gather,其他协程可能被取消(取决于是否已开始执行) - 设为
True后,所有结果(含Exception实例)按原顺序返回,你需要手动检查每个元素是不是异常 - 性能影响:设为
True不影响并发度,但会多保留异常对象,内存开销略增 - 容易踩的坑:以为
gather天然聚合异常,结果线上某个请求失败导致整批请求被截断
未捕获的异步异常会变成 RuntimeError 并打印到 stderr
当一个 task 内部抛出异常,又没人 await 它、也没人调用 task.exception(),Python 会在 event loop 关闭时把它作为“未处理异常”兜底处理:转成 RuntimeError,附带原始异常信息,输出到 stderr,但不中断主流程。
这种异常很难被日志框架捕获,也不进 except 块,常出现在后台任务、信号处理、或忘记清理的 task 中。
- 可通过
loop.set_exception_handler()自定义处理逻辑(比如上报 Sentry) - 开发期建议开启
asyncio.get_event_loop().set_debug(True),它会让这类异常立刻报 Warning,而不是等到 loop 结束 - 生产环境别依赖 stderr 日志——它可能被重定向、截断,或和其它输出混在一起
- 最稳妥的做法:所有
create_task()都配个 done callback 或包一层带日志的 wrapper
异步异常真正的复杂点不在语法,而在于“谁负责驱动、谁负责检查、谁负责清理”这三件事经常被拆开在不同模块里。一个 Task 创建于 A 模块,await 在 B 模块,错误处理逻辑在 C 模块——这时候异常传播路径就变成了协作契约,不是语言机制能兜住的。









