async/await异常不会自动冒泡,必须await协程或用asyncio.run()驱动才会触发;未await的任务异常会静默丢失或仅记录日志;Task.exception()可安全获取异常,而task.result()会重抛;asyncio.run()不捕获子任务异常;CancelledError需显式处理以确保资源释放。

async/await 中的异常不会自动向上冒泡
同步代码里 raise 会一直抛到最近的 try,但异步函数返回的是 coroutine 对象,异常实际被“封在”里面,直到你 await 它才真正触发。不 await 就相当于没执行,异常根本不会出现。
常见错误现象:async def f(): raise ValueError("boom") 单独调用 f() 不报错;只有 await f() 或 asyncio.run(f()) 才会看到异常。
- 所有异步入口(如
asyncio.run()、loop.create_task())都必须显式驱动协程,否则异常静默丢失 -
create_task()启动的任务如果未被await或asyncio.wait()等待,其异常会被记录到事件循环日志(Python 3.7+ 默认警告),但不会中断主流程 - 用
asyncio.gather(..., return_exceptions=True)可捕获子任务异常而不中断其他任务,返回的是包含Exception实例的列表
Task 对象的 exception() 方法是关键检查点
当你用 asyncio.create_task() 或 loop.create_task() 启动一个协程后,得到的是 Task 实例。它不会立即抛异常,而是把异常存在内部状态里,需主动查。
使用场景:批量启动多个任务,想等全部结束再统一处理结果和异常。
立即学习“Python免费学习笔记(深入)”;
- 任务完成前调用
task.exception()返回None;完成后若出错,返回实际异常对象;若成功,返回None - 不能用
task.result()替代 —— 它在异常状态下会直接重新抛出异常,可能崩掉当前上下文 - 搭配
asyncio.wait(tasks)后,遍历done集合,对每个task调用exception()最安全
asyncio.run() 的异常处理边界很明确
asyncio.run(coro) 是顶层入口,它会运行事件循环直到 coro 完成,并把该协程的异常原样抛出。但它**不捕获子任务(tasks)的异常** —— 那些未被等待的 task 异常只会触发 asyncio.exceptions.CancelledError 或日志警告。
容易踩的坑:
- 在
asyncio.run(main())的main()里用create_task()启了后台任务,但没await asyncio.gather(...)或await task,这些任务的异常不会让run()失败 -
asyncio.run()内部会调用loop.close(),如果此时还有 pending task,会引发RuntimeWarning: coroutine 'xxx' was never awaited - 测试时建议加
asyncio.run(main(), debug=True),能提前暴露未 await 的协程和静默异常
取消操作(Cancellation)和 CancelledError 的特殊性
asyncio.CancelledError 是唯一一个被设计为“预期中可忽略”的异常。它由 task.cancel() 触发,在 await 点被抛出,且默认不会打印 traceback —— 除非你在 except 块里显式处理或 raise 它。
为什么重要:它决定了异步清理逻辑是否可靠。
- 必须在
try/except中捕获CancelledError,并在finally或async with中做资源释放,否则取消可能导致句柄泄漏 -
asyncio.shield()可防止某个协程被取消,但它不阻止异常传播 —— 如果被 shield 的协程自己抛了别的异常,仍会照常冒泡 - 不要用
except BaseException:来吞掉CancelledError,否则task.cancel()就失效了
异步异常最难调试的地方不是“怎么抛”,而是“什么时候没抛”——静默失败比崩溃更危险。盯住 Task.exception()、别漏掉 await、在 run() 入口后加 debug=True,这三件事做完,大部分异常传播问题就浮出水面了。










