fetch 不报错却拿不到数据?404/500 不 reject 的原因与超时重试写法

fetch 只有网络层失败才会 reject,404/500 一样走 then,这就是「不报错却拿不到数据」的根源。本文给出 res.ok 检查封装、JSON 安全解析、AbortController 超时中止与指数退避重试的完整可运行代码,附一张五步排查对照表,帮你快速定位问题出在哪一层。

约 6 分钟读完 · 更新于 2026-10-21 · 文中每条都能在下面的配套工具里动手验证

先说结论:fetch 只在网络层失败时才 reject

HTTP 404、500、502 都是「请求成功到达服务器并得到了响应」,fetch 会正常走 then。想让业务代码拿到错误,必须自己检查 res.ok 或 res.status。这就是「不报错却拿不到数据」的第一大原因。

最小修复:把 res.ok 检查写进封装

一个 if 就能把静默失败变成显式异常,让 catch 真正接得住错误。

检查 res.ok 的最小封装
async function getJSON(url, options) {
  const res = await fetch(url, options);
  // 4xx / 5xx 不会 reject,必须手动抛错
  if (!res.ok) {
    throw new Error(`HTTP ${res.status} ${res.statusText}`);
  }
  return res.json();
}

getJSON('/api/user/1')
  .then(render)
  .catch(err => console.error(err.message)); // HTTP 404 Not Found

排查对照表:先对号入座

「拿不到数据」的表现千差万别,先在表里找到你的现象,再按一行修复处理。

现象常见原因一行修复
res.ok 为 falseURL 拼错、ID 不存在、权限不足打印 res.status 与实际请求的完整 URL 对照
res.json() 抛 Unexpected token接口返回了 HTML 404 页或空响应体先用 res.text() 打印原始响应,再决定怎么解析
then 里 data 是 undefined响应结构和你假设的不一样console.log 整个 data,确认字段路径
请求 pending 永远不返回服务端挂起、代理没有超时用 AbortController 设超时主动中止
CORS 报错且 status 为 0跨域被浏览器拦截检查响应头 Access-Control-Allow-Origin

res.json() 报错:先看原始文本再解析

res.json() 内部会调用 JSON.parse,只要响应体不是合法 JSON(常见是登录跳转的 HTML、错误页、空字符串)就直接抛 SyntaxError。稳妥做法是先取 text,再尝试 parse,失败时把前 200 个字符带进错误信息里。

安全解析 JSON
async function safeJSON(res) {
  const text = await res.text();
  try {
    return JSON.parse(text);
  } catch (e) {
    // 把响应片段带进错误里,方便定位是哪种页面
    throw new Error(
      `不是合法 JSON(HTTP ${res.status}): ${text.slice(0, 200)}`
    );
  }
}

五步定位「拿不到数据」

按顺序做,基本 5 分钟内能定位问题在哪一层。

  1. 打开 DevTools 的 Network 面板,确认请求真的发出去了、URL 和参数是否正确
  2. 看 Status 列:是 200 还是 404/500?非 2xx 就是服务端问题,先修接口或参数
  3. 看 Response / Preview 面板:内容是 JSON 还是 HTML?Content-Type 是 application/json 吗
  4. 是 200 但解析失败:把响应体复制出来本地验证,多数是字段层级和预期不符
  5. 以上都对但仍拿不到:检查 CORS、Cookie(credentials)与代理转发是否丢头

超时中止:AbortController 标准写法

5 秒超时
async function fetchWithTimeout(url, ms = 5000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);
  try {
    const res = await fetch(url, { signal: controller.signal });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } finally {
    clearTimeout(timer); // 无论成败都要清掉定时器
  }
}

// 被中止时抛出名为 "AbortError" 的 DOMException
fetchWithTimeout('/api/slow').catch(err => {
  if (err.name === 'AbortError') console.error('请求超时');
  else console.error(err.message);
});

重试不是无脑 for 循环

只有「值得重试」的失败才该重试,否则只会把故障放大。

  • 网络错误(fetch reject)和 5xx 可以重试;4xx 是请求本身的问题,重试还是 404
  • 加指数退避(1s → 2s → 4s),并加随机抖动避免同时重试造成雪崩
  • 给整条链路设置最大尝试次数(含首次),一般 3 次封顶
  • POST 等非幂等请求重试前想清楚:服务端可能已执行成功,需要幂等键
  • 页面卸载、用户已离开时不要再重试

带指数退避的重试封装

fetchRetry 完整实现
class RetryableError extends Error {}

async function fetchRetry(url, { retries = 3, backoff = 1000 } = {}) {
  for (let i = 0; i < retries; i++) {
    try {
      const res = await fetch(url);
      if (res.ok) return res.json();
      // 4xx 不值得重试,直接抛普通错误
      if (res.status < 500) throw new Error(`HTTP ${res.status}`);
      throw new RetryableError(`HTTP ${res.status}`);
    } catch (err) {
      const isLast = i === retries - 1;
      if (isLast || !(err instanceof RetryableError)) throw err;
      // 1s → 2s → 4s,加随机抖动防止同时重试
      const delay = backoff * 2 ** i + Math.random() * 300;
      await new Promise(r => setTimeout(r, delay));
    }
  }
}

一行修复:从今天起禁止裸 fetch().then(json)

99% 的「不报错拿不到数据」只要一个 if (!res.ok) throw new Error(res.status) 就能暴露出来。需要核对状态码语义时,用 HTTP 状态码查询工具确认 4xx/5xx 的准确含义,再把响应 JSON 粘进格式化工具看字段层级。

常见问题

fetch 请求 404 会抛异常吗?

不会。fetch 只在网络失败、请求被中止时 reject,404/500 属于正常响应,必须自己检查 res.ok 或 res.status 并手动抛错。

res.json() 报 Unexpected token 是什么问题?

响应体不是合法 JSON,常见是 404 HTML 页、登录跳转页或空字符串。先 await res.text() 打印前几百个字符确认内容再解析。

fetch 超时该怎么实现?

用 AbortController 配合 setTimeout,超时后调用 controller.abort(),fetch 会抛出 name 为 AbortError 的 DOMException。新浏览器也可直接用 AbortSignal.timeout(5000)。

哪些错误适合自动重试?

网络错误与 5xx 适合配合指数退避重试;4xx 是请求本身的问题,重试无意义,应直接暴露给业务层处理。

📂

松开以自动格式化

支持 .json / .sql / .csv / .html / .css / .xml / .yaml 文件