fetch 不会自动重试且 HTTP 错误不触发 catch,必须手动检查 response.ok 并抛错;正确做法是 await fetch 后立即 if (!res.ok) throw new Error(HTTP ${res.status}),再配合指数退避递归重试。

fetch 不会自动重试,HTTP 错误(如 404、500)也不会触发 catch——必须手动检查 response.ok 并主动抛错,否则重试逻辑根本不会启动。
为什么 fetch 失败了却没进 catch?
因为 fetch() 只在网络异常(断网、DNS 失败、AbortSignal 中止)时 reject;而服务端返回 404、500、401 等状态码时,它仍 resolve,仅把 response.ok 设为 false。
常见错例:fetch(url).then(r => r.json()),遇到 404 返回 HTML 页面时会抛 TypeError: Response body is not JSON,原始状态码和错误原因全丢失。
- 每次
await fetch(url)后必须立刻判断:if (!response.ok) throw new Error(`HTTP ${response.status}`) - 不加这一步,后续所有重试、日志、错误分类都失效
- 浏览器 DevTools 的 Network 面板里看到红标(4xx/5xx),但 JS 里没报错?八成是漏了这个判断
怎么写一个带退避的 fetchWithRetry 函数?
核心是递归 + 指数延迟 + 显式错误分类,避免用 while 循环或 setInterval,防止阻塞主线程或失控并发。
立即学习“前端免费学习笔记(深入)”;
将调研报告、方案对比、数据分析、需求梳理、会议纪要、设计说明、代码评审、学习笔记等任务产物输出为可在浏览器直接打开的单文件 HTML,替代 Markdown 作为最终交付物。Markdown 在信息密度、视觉清晰度和可分享性上已跟不上 AI 产物的丰富度,HTML 能同时承载排版、表格、SVG 等。
最小可用实现:
async function fetchWithRetry(url, options = {}, retries = 3) {
try {
const res = await fetch(url, { ...options, signal: AbortSignal.timeout(8000) });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res;
} catch (err) {
if (retries > 0 && (
err.name === 'TypeError' || // 网络级:断网、CORS、abort
err.message.startsWith('HTTP ') // HTTP 级:4xx/5xx
)) {
await new Promise(r => setTimeout(r, Math.pow(2, 3 - retries) * 1000));
return fetchWithRetry(url, options, retries - 1);
}
throw err;
}
}- 第 1 次重试延迟 1s,第 2 次 2s,第 3 次 4s(即
Math.pow(2, n)基础上调整偏移) - 单次请求强制超时:用
AbortSignal.timeout(8000)(Safari 16.4+ 支持;旧版需手写AbortController+setTimeout) - 只对两类错误重试:
TypeError(网络失败)和显式抛出的 HTTP 错误;其他如DOMException(用户手动 abort)不应重试
POST/PUT/DELETE 请求能直接重试吗?
不能无脑重试。非幂等请求重复发送可能造成数据重复(如下单两次、扣款两次),必须前置校验。
- 先确认接口是否声明幂等:查文档看是否有
Idempotency-Key头或服务端支持幂等 token - 带
credentials: 'include'的请求,若因 token 过期失败(如 401),重试前得先调refreshToken(),否则徒劳 - 按钮点击后立即
disabled = true,成功或最终失败后再恢复;否则用户连点会触发多轮并发重试 - 封装成函数调用(如
submitOrder()),而非在onclick里内联写fetch,避免闭包捕获过期变量
HTML 里怎么加“重试按钮”?
按钮本身不做事,关键是让它精准控制对应请求的重试逻辑,且防缓存、防重复、防状态错乱。
- 给目标资源加唯一标识,比如
<img id="avatar" data-id="user-avatar">,别靠 DOM 顺序或 class 名定位 -
onerror里记状态并启用按钮:this.dataset.failed = '1'; document.getElementById('retry-btn').disabled = false - 按钮点击时,先清空再赋值:
el.src = '';→el.src = '/api/avatar.jpg?'+Date.now()(或配cache: 'no-cache') - fetch 场景同理:按钮调用封装好的
loadData(),该函数内部已含重试逻辑和按钮禁用控制
最易被忽略的一点:重试不是兜底方案,而是针对“瞬时可恢复故障”的补救。如果连续三次都卡在 503 或 429,大概率是服务端真扛不住了——这时候该降级 UI、展示离线态,而不是继续刷请求。


















