资讯动态

Promise.all 中失败请求自动重试的正确解法:all-of-frontend 前端面试题精讲与工程化实践

发布时间:2026/10/15 6:03:51 来源:尧图企业网站定制
前端文档教程技术博客【免费下载链接】all-of-frontend你想知道的前端内容都在这项目地址https://gitcode.com/gh_mirrors/al/all-of-frontend点击查看免费下载导读本文围绕 all-of-frontend 仓库 Answer/1 ~ 10/5.md 记录的经典前端面试题展开多个异步请求通过Promise.all并发处理时如何让其中失败的所有请求自动重试。文章先剖析Promise.all的 fail-fast 语义与必须在请求函数内部 catch的核心结论再逐行拆解标准答案并从 Promise 状态采纳、异步递归、隐式类型转换等底层原理深入最后给出基于 axios 拦截器的工程化重试封装与Promise.allSettled备选方案。读完你将既能答对这道面试题也能把失败重试真正落地到业务请求层。题目回顾Promise.all 场景下的失败重试原题描述非常精炼核心诉求是让失败的所有请求重试Promise.all([A, B, C, D]) // 4 个请求完成后发现 AD 请求失败了如果让 AD 请求重试注意题目强调的是**失败的所有请求**——也就是失败的请求不止一个时如上例中的 A 和 D每个失败请求都要各自重试而不是重发整个批次。这与整体重试有本质区别也是很多错误答案的根源所在。核心分析为什么必须在请求函数内部处理 catchPromise.all 的 fail-fast 语义要理解这道题首先得清楚Promise.all的底层行为Promise.all接收一个可迭代的 Promise 集合并返回一个新的 Promise只有当所有Promise 都变为 fulfilled 时结果才会 resolve且结果数组顺序与传入顺序一致只要任意一个Promise 变为 rejectedPromise.all返回的 Promise 就会立刻以该错误 reject即 fail-fast其余尚未完成的请求结果被丢弃。所以题目的第一句结论是一个 Promise 在Promise.all中挂了就挂了——Promise.all本身不会给你任何重试机会它只会立刻把失败结果抛给下游。既然如此重试逻辑就绝不可能放在Promise.all的外部比如给它整体挂一个.catch后重发全部请求因为那样会把已经成功的请求也重新请求一遍既浪费流量又可能产生重复副作用重复下单、重复写库等。常见错误解法在 resolve 里判断重试原文档明确指出网上部分答案结果是对的但处理方式是错误的典型代表是在resolve中判断是否需要重试。例如有人会写类似下面的伪逻辑new Promise((resolve) { request().then((data) { if (需要重试的条件) { return request(); // 再发一次 } resolve(data); }); });这个思路错在哪原文档给了一针见血的解释业务中的请求基本都是封装过的函数出错时一定走reject根本不可能resolve出一个错误来。换句话说一个遵循 Promise 语义的请求封装成功与失败只有resolve/reject两条出路错误信息通过 rejection reason 传递。如果某个封装把失败也resolve出来比如resolve({ code: 500 })那它本身就是反模式——调用方无法用统一的.catch处理错误接入第三方库、做错误监控时都会非常别扭。因此在resolve里做假成功判断既不符合封装约定也无法覆盖真实业务中封装函数直接reject的情况。正确的拦截点只有一个请求函数内部的.catch。因为每个请求的失败在封装内部就被拦截并重试Promise.all拿到的永远是这个请求的最终结果成功或重试耗尽后的失败。标准解法在请求封装内通过 catch 链式重试完整代码原文档给出的标准答案摘自社区 Issue 的优秀作答如下function request(name, count 0) { return new Promise((resolve, reject) { const isSuccess Math.random() 0.5; console.log(接口${name}: ${isSuccess}); setTimeout(() { isSuccess 0.5 ? resolve(name) : reject(name); }, Math.random() * 1000); }).catch((err) { count; if (count 2) { return Promise.reject(后端大爷${name}接口写的666); } return request(name, count); }); } let queue [request(A), request(B), request(C), request(D)]; Promise.all(queue) .then((arr) { console.log(arr); }) .catch((err) { console.log(err); });代码拆解这份代码的骨架可以提炼为三层模拟请求层request(name, count)用一个Promise模拟异步接口Math.random() 0.5决定本次请求成败setTimeout模拟网络耗时。这是面试演示用的模拟实现真实业务中这里应替换为axios/fetch等真实请求。重试层Promise链末尾挂的.catch((err) {...})是整道题的灵魂。任何一次失败都会进入这里然后做两件事count累计失败次数若count 2说明重试次数已达上限返回Promise.reject(...)把最终失败抛给Promise.all否则return request(name, count)带着新的计数重新发起请求。聚合层Promise.all(queue)等待四个请求的最终结果——注意是每个请求内部完成重试之后的结果。全部成功则then输出结果数组任一请求重试耗尽仍失败则catch收到错误。执行流程推演以A、D 首次请求失败为例完整流程是A、D 首次失败后分别进入各自的.catchcount从 0 变为 1随即发起第二次请求若 A 第二次仍失败count变为 2继续发起第三次请求此时重试次数还没超过上限若 A 第三次仍失败count变为 3触发count 2A 的最终状态变为 rejectedPromise.all收到该错误并整体 reject若 D 第二次成功则 D 正常 resolve只要四个请求的最终结果全部 resolvePromise.all的then才会执行输出[A, B, C, D]。由此也回答了题目本身失败的所有请求是如何被覆盖的——重试逻辑写在每个请求自己的封装里A、D 各自独立重试互不影响。这正是该方案相比整体重发的本质优势。深入原理catch 返回的 Promise 如何驱动重试Promise 状态采纳这道题能成立依赖的是 Promise 的**状态采纳assimilation**机制.catch(onRejected)会返回一个新的 Promise当onRejected内部返回一个 Promise 时外层 Promise 的状态会被采纳——也就是说.catch里return request(name, count)之后外层 promise 会跟随重试请求的状态走重试成功则整体 resolve重试失败则整体 reject重试返回的 promise 再 reject 又会再次进入下一个.catch…… 这正是链式重试能够循环往复的底层原理。Promises/A 规范与 ECMAScript 的 Promise 语义均规定了这一行为仓库 README.md 的 Promise 学习路径里也收录了 Promises/A 规范原文与手写实现20 行极简版、100 行规范版等资料可对照研读。异步递归不会爆栈return request(name, count)表面看是递归调用自身但它发生在.catch回调里且每次重试都经由setTimeout异步调度、每次都新建 Promise。也就是说这是异步递归重试 N 次只是多挂了几层微任务/宏任务链并不会在同步调用栈上层层累积因此不存在递归深度导致栈溢出的问题。读者如果担心这点可以放心。一个容易被忽略的细节布尔值与数值比较细心的读者可能注意到原代码里isSuccess是布尔值Math.random() 0.5的结果但判断条件写的却是isSuccess 0.5。这在语法上是合法的JS 在做布尔与数值比较时会把布尔值隐式转换为 0/1即true 0.5等价于1 0.5false 0.5等价于0 0.5。所以这段代码碰巧能正确工作但可读性并不好面试或落地时建议直接写成isSuccess ? resolve(name) : reject(name)。顺带一提重试上限的判断count 2意味着每个请求最多重试2次、总尝试3次这个阈值应根据业务容忍度调整而不是写死。工程化扩展把重试能力封装进请求层面试题之外真正的价值在于把失败重试做成请求层的通用能力。仓库 package.json 声明了axios ^0.21.1依赖说明请求封装是这类场景的标配基础设施下面给出基于 axios 拦截器的通用重试封装示例通用实践模式可直接复制改造。重试策略三要素工程化的重试绝不是简单的失败就再来一次至少要决策三件事最大重试次数maxRetries决定容忍几次失败防止无限重试拖垮系统重试间隔delay立即重试往往与首次失败原因高度相关如瞬时网络抖动适度等待能提高成功率退避策略backoff固定延迟简单直接指数退避每次延迟翻倍更符合故障持续期未知的实际情况再加上随机抖动jitter可以避免大量请求在同一时刻集中重试造成雪崩。一个典型的指数退避 抖动实现function backoffDelay(attempt, base 500, max 10000) { const exp Math.min(base * Math.pow(2, attempt), max); return exp / 2 Math.random() * (exp / 2); // 加随机抖动 }axios 拦截器版重试封装把重试挂在响应拦截器的错误分支上业务侧调用方式零改动function createRequestWithRetry(maxRetries 2, baseDelay 500) { const instance axios.create(); instance.interceptors.response.use( (res) res, (err) { const config err.config || {}; // 用自定义字段记录当前请求已重试次数 config.__retryCount (config.__retryCount || 0) 1; if (config.__retryCount maxRetries || !shouldRetry(err)) { return Promise.reject(err); } const delay backoffDelay(config.__retryCount, baseDelay); return new Promise((resolve) setTimeout(resolve, delay)).then(() instance(config) // 重新发起原请求 ); } ); return instance; }配套的shouldRetry决定哪些错误值得重试function shouldRetry(err) { if (err.code ECONNABORTED) return true; // 超时 if (!err.response) return true; // 网络层错误无响应 const status err.response.status; return status 500 status 600; // 5xx 服务端错误 // 4xx 属于客户端参数问题重试无意义默认不重试 }重试的边界与约束工程上重试必须考虑副作用问题只有幂等请求查询类 GET、幂等键保护的写操作才适合自动重试对于下单、转账等非幂等写操作自动重试可能造成重复提交应改为人工确认或配合幂等键。此外重试会放大请求量务必设置总重试次数与退避上限防止服务端故障时客户端疯狂重试。另一条路线Promise.allSettled 收集失败再重试题目给的是每个请求内部各自重试的解法但面试官常会追问如果我不想改请求封装有没有别的思路。此时可以引出Promise.allSettled的备选方案async function runWithRetry(requests, maxRetries 2) { let pending requests.map((p) p().catch((e) ({ error: e }))); let attempt 0; while (attempt maxRetries) { const results await Promise.allSettled(pending.map((p) p)); const failed results.filter((r) r.status rejected); if (failed.length 0) { return results.map((r) (r.status fulfilled ? r.value : null)); } // 只对失败的请求重新发起 pending failed.map((r) r.reason()); attempt; } throw new Error(重试次数耗尽); }两种方案的取舍很清晰请求内部重试题目标准解重试逻辑与请求封装绑定适合所有调用方都要重试的通用场景但侵入封装外层收集重试allSettled 路线不改封装重试策略由调用方按业务临时决定代价是Promise.allSettled会等待所有请求落定、失败项要自己维护索引与重发逻辑代码更啰嗦。回答时能讲清这两种路线及各自适用场景比背答案更能体现功底。验证思路与自测建议原题代码用Math.random()模拟成败运行结果不确定只适合演示。要真正验证重试逻辑建议把成败来源抽成可注入的参数例如传入一个预先排好的成败序列让测试可复现function createRequestWithOutcome(name, outcomes) { let index 0; return function request(count 0) { return new Promise((resolve, reject) { const ok outcomes[index] ?? false; setTimeout(() (ok ? resolve(name) : reject(name)), 10); }).catch(() { if (count 1 2) return Promise.reject(name); return request(count 1); }); }; } // A失败→失败→成功第 3 次才成功D失败→成功 Promise.all([ createRequestWithOutcome(A, [false, false, true])(), createRequestWithOutcome(B, [true])(), createRequestWithOutcome(C, [true])(), createRequestWithOutcome(D, [false, true])(), ]) .then((arr) console.log(成功:, arr)) // 应输出 [A,B,C,D] .catch((e) console.log(失败:, e));自测时可以覆盖四类用例全部首次成功、部分重试后成功、重试耗尽整体失败、失败请求数量大于 1验证所有失败请求各自重试。若使用 Jest可配合jest.useFakeTimers()固定setTimeout时序让断言完全确定。关联学习路径本题是仓库前端核心学习路径 → JS 核心知识点 → Promise这一环的实战延伸。仓库 README.md 中 Promise 小节收录了 MDN 文档、Promises/A 规范及多篇手写 Promise 实现文章建议把规范语义状态采纳、fail-fast与手写实现对照学习。仓库内还有多道 Promise 相关的姊妹题值得一并练习Answer/1 ~ 10/4.md点击按钮按顺序输出请求结果的队列题利用promise.then实现串行队列与本题并发 重试互为对照Answer/1 ~ 10/1.md、Answer/1 ~ 10/2.mdasync函数内部错误与try/catch捕获范围的辨析能帮你理解为什么在async外层 catch 抓不到异步错误Answer/11-20/17.mdawait与加法运算符求值顺序的陷阱属于 async/await 语义的进阶补充。把这些题目串起来Promise、async/await、错误处理与并发控制这几块知识就形成了一个完整闭环无论是面试还是日常请求层开发都能做到知其然且知其所以然。赞分享前端文档教程技术博客【免费下载链接】all-of-frontend你想知道的前端内容都在这项目地址https://gitcode.com/gh_mirrors/al/all-of-frontend点击查看免费下载相关推荐all-of-frontend 面试题精讲用 Promise 链式调用实现并发请求、有序输出的异步队列all of frontend 面试题精讲用 Promise 链式调用实现并发请求、有序输出的异步队列 导读本文围绕 all of frontend 仓前端文档教程技术博客Requests重试流程自动重试失败请求Requests重试流程自动重试失败请求 在日常的网络请求中我们经常会遇到各种不稳定的情况比如网络波动、服务器临时不可用等这些问题可能导致请求失败。如果后端网络通信all-of-frontend 面试题精讲this 默认绑定、arguments 对象与 window.length 的三重陷阱all of frontend 面试题精讲this 默认绑定、arguments 对象与 window.length 的三重陷阱 本文以 all of fro前端文档教程技术博客上一篇3步快速上手YimMenu GTA5辅助工具完全配置指南下一篇Spatie Laravel Query Builder 安装与配置指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑