AbortSignal.timeout 与组合取消
超时和用户取消都是 abort,但原因不同。代码必须读取 signal.reason 或捕获异常名称,才能给用户正确反馈。
最小实验:区分超时与用户取消
下面的操作模拟一个可以被信号取消的异步任务,不需要后端:
js
function delayedTask({ signal, delay = 1000 }) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => resolve('done'), delay)
const cancel = () => {
clearTimeout(timer)
reject(signal.reason ?? new DOMException('Aborted', 'AbortError'))
}
if (signal.aborted) cancel()
else signal.addEventListener('abort', cancel, { once: true })
})
}
const user = new AbortController()
delayedTask({ signal: AbortSignal.any([user.signal, AbortSignal.timeout(100)]) })
.then(console.log)
.catch(error => console.log('timeout:', error.name, error.message))
const manual = new AbortController()
delayedTask({ signal: manual.signal })
.catch(error => console.log('manual:', error.name, error.message))
setTimeout(() => manual.abort(new DOMException('User cancelled', 'AbortError')), 20)第一条应打印 timeout: TimeoutError ...,第二条应打印 manual: AbortError ...。把 100 毫秒改成 2000,再观察用户取消先发生时 AbortSignal.any() 保留用户的 reason。生产代码调用真实 fetch(url, { signal }) 时沿用同样的分类。
机制与边界
AbortSignal.timeout() 产生的 reason 是 TimeoutError;用户控制器可以使用 AbortError 或业务自定义 reason。AbortSignal.any() 只组合信号,不会撤回已经发送到服务器的副作用。长期定时任务若需要主动清理,应使用可 clearTimeout 的控制器,而不是把超时当作唯一释放机制。
练习
- 给任务增加成功、超时、用户取消和不支持
AbortSignal.timeout的降级路径。 - 记录
signal.aborted、signal.reason和 catch 到的错误,说明三者的关系。 - 说明为什么写请求还需要幂等设计。