Skip to content

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 的控制器,而不是把超时当作唯一释放机制。

练习 ​

  1. 给任务增加成功、超时、用户取消和不支持 AbortSignal.timeout 的降级路径。
  2. 记录 signal.aborted、signal.reason 和 catch 到的错误,说明三者的关系。
  3. 说明为什么写请求还需要幂等设计。

查看本节答案

用心记录每一步成长