cancellederror只能在await语句所在的协程内部捕获,无法在外层try/except中拦截;它由事件循环注入挂起点,需在协程内用try包围await并处理,finally不可靠,清理应放在except asyncio.cancellederror中。

CancelledError只能在await语句的协程栈中捕获
你没法在 await 外层用 try/except 捕获 CancelledError —— 它不是普通异常,而是由事件循环主动注入到被取消协程的当前暂停点的。一旦协程在 await 处挂起,事件循环若决定取消它,就会在恢复执行前直接抛出 CancelledError,且只会在那个 await 表达式所在协程的上下文中可见。
常见错误写法:
try:
await asyncio.sleep(1) # ← 错误地以为外层 try 能兜住
except asyncio.CancelledError:
print("不会执行到这里")
这看起来像能捕获,但实际运行时,如果该任务被取消,CancelledError 就会从 await 内部直接冒泡出来,而外层 try 根本没机会介入——因为协程根本没“执行到” except 块,它被中断在 await 点上。
必须在协程函数内部用try包围await语句
正确做法是把 try/except 放进协程函数体里,并确保它包裹的是触发挂起的 await 表达式本身(或其直接外层作用域):
async def fetch_data():
try:
await asyncio.sleep(2)
return "done"
except asyncio.CancelledError:
print("任务被取消,正在清理...")
# 这里可以做释放资源、关闭连接等操作
raise # 通常建议重新抛出,让调用方知道已取消
- 如果你不
raise,协程会静默结束,上级asyncio.gather(..., return_exceptions=False)会把它当普通完成处理,可能掩盖问题 - 如果协程里有多个
await,每个都可能被取消,所以最好把整段逻辑包在一个try里,或按需分段处理 -
asyncio.shield()可以临时防止某次await被取消,但不能替代异常处理逻辑
在Task.cancel()后await task时也会抛CancelledError
当你手动调用 task.cancel(),再 await task,这个 await 本身就会抛 CancelledError(哪怕原协程里没写 try):
task = asyncio.create_task(fetch_data())
task.cancel()
try:
await task # ← 这里会直接 raise CancelledError
except asyncio.CancelledError:
print("task.await 抛了CancelledError")
这种场景下,try 必须套在 await task 这一行上,而不是原协程里——因为原协程可能早已退出,事件循环是在等待 task 结果时注入异常的。
- 不要混淆「协程内被取消」和「等待一个已被取消的Task」两种路径
-
asyncio.wait_for(..., timeout=...)超时时也会取消内部 task,因此它的await同样可能抛CancelledError,需同样防护 -
asyncio.create_task()创建的任务,即使没显式await,只要它还在运行中,就可能被取消并抛异常
容易忽略的清理时机:__aexit__ 和 finally 不可靠
finally 块在协程被取消时**不一定执行**——只有当协程已恢复并走到 finally 所在位置才会运行;如果卡在 await 上就被取消,finally 就跳过了。
真正可靠的清理方式只有两种:
- 在
except asyncio.CancelledError:中做清理,然后raise - 用异步上下文管理器(
async with),确保__aexit__被调用(前提是上下文管理器本身没被跳过)
例如数据库连接、WebSocket 链接这类资源,别依赖 finally 关闭,得在 CancelledError 处理分支里显式 await conn.close()。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











