← 中文资源

简体中文版本

Fetch 重试中 AbortError 掩盖了真实错误

避免重试逻辑把超时控制器有意触发的 AbortError 当成网络故障,并保留原始错误原因。

阅读英文原文

症状

错误“Fetch 重试中 AbortError 掩盖了真实错误”指向一个明确的失效模式。下面依次给出症状、定位该原因的检查命令、根本原因、修复方案,以及证明修复有效的验证步骤。

可观察到的症状包括:

  • 重试日志记录的是 AbortError,而非底层超时或响应错误
  • 操作重试多次后才抛出笼统的失败
  • 错误监控显示 AbortError 却无根因

诊断

先在隔离环境中复现故障,再按顺序执行以下检查,逐步缩小范围。

步骤 1

if (err.name === 'AbortError') { /* intentional */ } else { retry(err) }

来自自有控制器的 AbortError 是超时的预期结果,应映射为超时错误,而非进入重试循环。

步骤 2

controller.abort(new Error('timeout after 5s'))

传入 reason 可使超时显式化,调用方可准确上报。

常见诱因(按出现频率排序):

  • catch 块把任何抛出的错误(含 AbortError)都当作可重试
  • 超时控制器中止请求,却从未读取中止原因
  • 原始错误被替换,而非通过 cause 链式保留

根因

AbortController.abort() 会以名为 AbortError 的 DOMException 拒绝 fetch promise。仅凭消息文本判断的重试逻辑无法区分主动中止与传输故障,于是无谓重试或上报错误。

修复

按错误名分类捕获:把 AbortError 视为终态超时,仅对网络错误与 5xx 重试,并把原始错误作为 cause 链式保留,让监控保留真实信号。

注意:盲目重试非幂等的 POST 可能重复产生副作用;仅对幂等操作重试,或使用幂等键。

async function fetchWithTimeout(url, ms = 5000) {
  const c = new AbortController();
  const timer = setTimeout(() => c.abort(new Error('request timed out')), ms);
  try {
    return await fetch(url, { signal: c.signal });
  } finally {
    clearTimeout(timer);
  }
}

async function attempt(url, tries = 3) {
  for (let i = 1; i <= tries; i++) {
    try {
      const res = await fetchWithTimeout(url);
      if (res.ok || res.status < 500) return res;
      throw new Error(`HTTP ${res.status}`);
    } catch (err) {
      if (err.name === 'AbortError' || i === tries) throw err;
      await new Promise((r) => setTimeout(r, i * 200));
    }
  }
}

验证

强制超时并确认日志显示超时错误而非 AbortError;强制返回 500 并确认重试次数恰为配置值。

修复后补充一条回归测试,复现原始症状,防止问题再次出现。

这类故障通常是环境问题而非纯逻辑错误,因此在缺陷记录中应写明完整命令、环境变量与工作目录。请用最小的脚本复现,待最小用例清楚后再扩大范围。为这类故障准备一份简短的排查清单,能显著减少下次排障时间。