先记住这个答案
Promise.all 的规则是只要有一个任务 reject,它就立刻 reject,已经成功的结果也拿不到。想让其他结果可用,就要在任务汇总之前把失败拦下来。第一种办法是给每个 Promise 接一个 .catch(),失败时返回一个兜底值,这样传给 Promise.all 的全是会成功的 Promise。第二种办法是直接用 Promise.allSettled,它等所有任务都结束,每一项用 status 告诉你成功还是失败。
Promise.all一项失败就整体 reject,成功结果全丢- 每项先
.catch()兜底,失败变成普通值 Promise.allSettled天生不会 reject,按status区分- 关键接口用
Promise.all,可选接口单独兜底
给每个请求单独兜底
比如一个首页同时拉用户信息、订单、推荐三个接口。推荐接口挂了,你肯定不希望整个首页白屏,用户和订单照样要展示。
.catch() 再交给 Promise.allJavaScriptconst request = (name, ok, ms) =>
new Promise((resolve, reject) => {
setTimeout(() => (ok ? resolve(name + '数据') : reject(new Error(name + '接口 500'))), ms)
})
async function loadPage() {
const tasks = [request('用户', true, 30), request('订单', false, 10), request('推荐', true, 20)]
// 给每个任务单独兜底:失败时返回一个带标记的对象,不再往外抛
const safe = tasks.map((p) => p.catch((err) => ({ error: err.message })))
const results = await Promise.all(safe) // 永远不会 reject
results.forEach((r) => console.log(typeof r === 'string' ? r : '失败:' + r.error))
}
loadPage()查看输出与解释
用户数据
失败:订单接口 500
推荐数据关键在 p.catch(...) 这一步。catch 返回的是一个新的 Promise,只要回调里没有再抛错,这个新 Promise 就是 fulfilled,值是回调的返回值。所以订单接口虽然失败了,到 Promise.all 手里已经变成了一个普通对象 { error: '订单接口 500' }。结果数组的顺序和传入顺序一致,不受谁先完成影响。
这种写法的好处是兜底值由你决定。比如推荐接口失败就返回空数组 [],渲染代码不用改,页面上推荐区块显示为空就行。
直接用 Promise.allSettled
ES2020 加入的 Promise.allSettled 就是为这个场景设计的。它不会 reject,等所有任务都落定后,返回一个数组,每一项是 { status: 'fulfilled', value } 或者 { status: 'rejected', reason } 这样的对象。
status 把成功和失败分开JavaScriptconst request = (name, ok, ms) =>
new Promise((resolve, reject) => {
setTimeout(() => (ok ? resolve(name + '数据') : reject(new Error(name + '接口 500'))), ms)
})
Promise.allSettled([request('用户', true, 30), request('订单', false, 10), request('推荐', true, 20)])
.then((results) => {
const ok = results.filter((r) => r.status === 'fulfilled').map((r) => r.value)
const failed = results.filter((r) => r.status === 'rejected').map((r) => r.reason.message)
console.log('成功:' + ok.join('、'))
console.log('失败:' + failed.join('、'))
})查看输出与解释
成功:用户数据、推荐数据
失败:订单接口 500和单项 .catch() 比,Promise.allSettled 保留了原始的错误对象,适合要上报错误、要统计失败率的场景。缺点是拿到结果后还得自己拆一遍,不像兜底值那样可以直接用。
- 两种写法怎么选
- 需要给失败项一个默认值、让后面代码照常跑,用单项
.catch()。需要知道到底哪些失败了、失败原因是什么,用Promise.allSettled。两者在「保住其他结果」这件事上效果一样。
关键接口和可选接口混在一起
真实页面里接口的地位不一样。比如商品详情页,商品信息接口挂了,页面就没法看,应该整体报错;评论、推荐挂了,只影响一个区块。
这时不要一刀切。关键接口保持原样,可选接口各自接 .catch() 返回默认值,再一起放进 Promise.all。这样商品接口失败时整体 reject,走错误页;评论接口失败时整体照常成功,评论区显示「暂无评论」。一个 Promise.all 就把两种策略都表达清楚了。
容易答错的地方
- 以为
Promise.all失败后其他请求会被取消 - 不会。Promise 没有取消机制,
Promise.allreject 只是不再关心其他结果,那些请求照样发完,服务端照样处理。真要中断,得配合AbortController手动调用abort()。 - 兜底时返回
null,下游分不清失败和空数据 p.catch(() => null)看起来简洁,但如果接口正常时也可能返回null,下游就不知道到底是失败还是真没数据。兜底值最好带上标记,比如{ error }对象,或者直接用Promise.allSettled。- 在
Promise.all外面包try...catch就以为能保住结果 try...catch能接住Promise.all的拒绝,但进到catch时你手里只有那一个错误,成功的结果已经没了。拦截必须发生在每个任务上,不能发生在汇总之后。
面试官还会怎么问?
不支持 Promise.allSettled 的环境怎么办?
用 Promise.all 加单项 .catch() 就能模拟出来:每个 Promise 接 .then(value => ({ status: 'fulfilled', value }), reason => ({ status: 'rejected', reason })),再交给 Promise.all。这也是很多 polyfill 的实现思路。
失败的那个请求想自动重试怎么写?
在单项兜底的位置换成重试逻辑:先调用一次,catch 里判断次数,没到上限就再调用自己,到了上限再返回兜底值。这样重试只影响那一个任务,其他任务的结果照常汇总。
只要有一个成功就行,该用哪个方法?
用 Promise.any。它和 Promise.all 刚好相反:有一个成功就兑现,全部失败才以 AggregateError 拒绝。适合多个镜像地址取最快可用那个的场景。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。