登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

Fetch AbortController 如何取消超时请求

来源:17golang原创

时间:2026-09-12 16:48:56 108浏览 收藏

给前端 fetch 加超时,不是给 Promise 外面套一个 setTimeout 就结束了。真正要中止网络请求,需要把 AbortSignal 传进 fetchsignal 选项;超时后由信号触发取消。现代浏览器可以直接使用 AbortSignal.timeout(ms),并在捕获异常时把 TimeoutError 和用户主动取消产生的 AbortError 分开处理。

要点速览
  • 每次请求创建新的 AbortController 或超时信号,已终止的信号不能复用。
  • AbortSignal.timeout(5000) 会在超时后终止 fetch,但不会把 HTTP 500 自动变成异常。
  • 捕获异常时优先判断 TimeoutErrorAbortError,最后再处理网络错误或其他异常。
  • 需要兼容较旧浏览器或明确清理定时器时,可用 AbortController + setTimeout 回退。

1. 为每次请求创建独立的 AbortSignal

AbortController 负责发出取消动作,controller.signal 是传给异步 API 的只读信号。请求开始前创建它,调用 controller.abort() 后,使用这个信号的 fetch 会被拒绝。

控制器不要放成一个永久复用的全局变量。一个信号一旦进入 aborted 状态,后续再把它交给新的请求,新请求会立即失败。独立创建也能避免“搜索框上一次请求取消了下一次请求”的串扰。

async function requestJson(url, controller) {
  // 每个请求使用自己的 signal,避免复用已经 aborted 的信号。
  const response = await fetch(url, { signal: controller.signal });

  // fetch 遇到 404/500 仍可能正常返回 Response,这里显式检查状态。
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  // 读取 body 也属于异步阶段,读取前后都可能受到取消影响。
  return response.json();
}

const controller = new AbortController();
requestJson('/api/profile', controller)
  .catch((error) => console.error(error));

// 需要取消时调用;生产代码通常由按钮、路由切换或组件卸载触发。
// controller.abort();
前端 Fetch 请求中 AbortController、AbortSignal 与响应体读取的关系示意图
图1:操作示意图,展示独立 AbortController 如何把取消信号传入 Fetch 请求。

2. 用 AbortSignal.timeout 绑定请求超时

只关心“超过 5 秒就停止”的场景,可以把超时信号直接传给 fetch。它比在外层竞争两个 Promise 更可靠,因为实际的 Fetch 操作确实收到了终止信号。

async function fetchWithTimeout(url, timeoutMs = 5000) {
  // timeout 到期后 signal 会自动 aborted,时间单位是毫秒。
  const signal = AbortSignal.timeout(timeoutMs);
  const response = await fetch(url, { signal });

  // 网络请求成功不等于业务状态成功,继续检查 HTTP 状态码。
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  // body 读取也受同一个 signal 影响,不能只保护 fetch 调用本身。
  return response.json();
}

这里有一个容易误判的边界:fetch 只会在请求失败或被取消时拒绝,服务器返回 404、500 等 HTTP 状态并不会自动进入 catch。因此超时取消与 HTTP 错误必须分别处理。

3. 按异常名称区分超时、主动取消和网络失败

AbortSignal.timeout() 超时通常以名称为 TimeoutErrorDOMException 拒绝;调用控制器的 abort() 则通常对应 AbortError。不要只写一个“请求失败”的提示,否则用户主动离开页面也会被记录成网络故障。

async function loadUser(url) {
  try {
    const response = await fetch(url, {
      // 让浏览器在 8 秒后终止仍未完成的请求。
      signal: AbortSignal.timeout(8000),
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    // TimeoutError 表示自动超时,可以提示用户稍后重试。
    if (error.name === 'TimeoutError') {
      throw new Error('请求超过 8 秒仍未完成');
    }

    // AbortError 更接近用户取消、页面离开等主动终止。
    if (error.name === 'AbortError') {
      return null;
    }

    // 其他错误可能是网络断开、CORS 或代码问题,保留原始原因用于排查。
    throw error;
  }
}

如果还要判断信号状态,可以检查 signal.abortedsignal.reason。但不要用“进入了 catch”作为超时判断,因为 DNS、CORS、断网和解析失败同样可能进入 catch。

Fetch 请求按 TimeoutError、AbortError、HTTP 状态和网络错误分流的结果示意图
图2:结果示意图,展示超时、主动取消、HTTP 错误和网络错误应分别进入不同处理分支。

4. 合并用户取消与自动超时

搜索建议、文件下载和路由切换常常同时需要两种取消来源:用户点击取消,或者请求超过预算自动停止。可以用 AbortSignal.any() 合并它们,任意一个信号终止都会让 Fetch 停止。

async function fetchCancelable(url, cancelButton, timeoutMs = 5000) {
  const userController = new AbortController();
  const timeoutSignal = AbortSignal.timeout(timeoutMs);

  // 任一来源终止,combinedSignal 都会终止当前 fetch。
  const combinedSignal = AbortSignal.any([
    userController.signal,
    timeoutSignal,
  ]);

  const onCancel = () => userController.abort();
  cancelButton.addEventListener('click', onCancel, { once: true });

  try {
    const response = await fetch(url, { signal: combinedSignal });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return await response.json();
  } finally {
    // 请求结束后移除自定义监听,避免长期持有按钮和闭包引用。
    cancelButton.removeEventListener('click', onCancel);
  }
}

any() 的便利之处是统一入口,代价是最终信号只表达“已经取消”,不能可靠地告诉你是哪个输入先触发。若产品必须精确记录“用户取消”和“超时”两个指标,可以保留自己的 timedOut 标记,或使用手动控制器加定时器的方案。

5. 兼容回退与发布前检查清单

较旧环境不一定支持 AbortSignal.timeout()。这时使用普通 AbortController,在 finally 中清理定时器即可;这种写法还可以明确取消超时计时器。

async function fetchWithFallback(url, timeoutMs = 5000) {
  const controller = new AbortController();
  let timedOut = false;

  // 定时器只负责触发 abort,真正的请求终止仍由 signal 传递。
  const timer = setTimeout(() => {
    timedOut = true;
    controller.abort();
  }, timeoutMs);

  try {
    const response = await fetch(url, { signal: controller.signal });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return await response.json();
  } catch (error) {
    // 回退方案需要自己标记超时,因为异常名称通常只是 AbortError。
    if (timedOut) throw new Error('请求超时');
    if (error.name === 'AbortError') return null;
    throw error;
  } finally {
    // 成功、失败、取消都要清除计时器,避免无意义的回调继续执行。
    clearTimeout(timer);
  }
}
现象优先判断处理建议
超过预算后停止TimeoutError 或自定义 timedOut提示重试,记录耗时
用户点取消或路由离开AbortError通常静默结束,不当成系统故障
返回 4xx/5xxresponse.ok按业务状态展示服务端错误
断网、CORS、解析异常其他异常保留原始错误并进入通用故障处理

上线前至少检查五件事:信号是否每次请求独立创建;signal 是否真的传入 fetch;响应状态是否单独判断;响应体读取是否仍在取消保护范围内;自定义事件和定时器是否在 finally 中清理。这样处理后,Fetch 的超时、取消和网络失败就不会再被一条模糊的“请求失败”吞掉。

常见问题

AbortSignal.timeout 的参数单位是什么?

单位是毫秒,例如 AbortSignal.timeout(5000) 表示约 5 秒的活动时间预算。

fetch 返回 500 会触发 catch 吗?

不会仅因为状态码触发。先检查 response.okresponse.status,再按业务抛出异常。

一个 AbortController 能给多个请求使用吗?

可以让多个请求共享同一个取消动作,但控制器一旦 abort,所有关联请求都会被终止;已终止的 signal 不适合启动新请求。

AbortSignal.any 能告诉我是谁触发取消吗?

它适合统一取消入口,但不能可靠区分最终来源。需要统计来源时,额外维护用户取消标记和超时标记。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>