先记住这个答案
会立即拒绝。Promise.all 只要看到任何一项 reject,就用这个错误拒绝自己,不再等其他项。但其他任务并不会被停止,它们在创建 Promise 的时候就已经开始执行了,Promise.all 只是不再收集它们的结果。之后即使别的任务也失败了,这些错误也会被默默忽略,不会触发未处理拒绝的警告。
- 第一个 reject 出现,
Promise.all立刻拒绝 - 其他任务照样执行,结果被丢弃
- 后续的 reject 被忽略,不会报未处理错误
- 想真正停掉剩余请求,要用
AbortController
看一下执行顺序
三个任务分别在 30、10、20 毫秒后完成,其中 B 和 C 都会失败。
const task = (name, ms, fail) =>
new Promise((resolve, reject) => {
setTimeout(() => {
console.log(name, '执行完')
fail ? reject(new Error(name + ' 失败')) : resolve(name)
}, ms)
})
Promise.all([task('A', 30), task('B', 10, true), task('C', 20, true)])
.then((res) => console.log('then', res))
.catch((err) => console.log('catch', err.message))查看输出与解释
B 执行完
catch B 失败
C 执行完
A 执行完10 毫秒时 B 失败,catch 马上执行,打印「catch B 失败」,这就是快速失败。但 C 和 A 的定时器没有被取消,后面照样打印「执行完」。C 也 reject 了,可是 Promise.all 早就落定了,Promise 的状态一旦改变就不会再变,所以 C 的错误没有任何效果。
为什么 C 的错误没有报「未处理的拒绝」?因为 Promise.all 一开始就给每一项都挂了成功和失败两个处理函数,C 的拒绝其实是被处理过的,只是处理的结果被丢掉了。
怎么真正停掉剩下的请求
真实项目里,其他任务通常是网络请求。一个失败了,剩下的请求还在占带宽、占服务端资源。如果剩下的结果已经没用了,可以用 AbortController 主动取消。
signal,失败时一起取消JavaScriptasync function loadAll(urls) {
const controller = new AbortController()
try {
return await Promise.all(
urls.map((url) =>
fetch(url, { signal: controller.signal }).then((res) => {
if (!res.ok) throw new Error(`${url} 返回 ${res.status}`)
return res.json()
})
)
)
} catch (err) {
controller.abort() // 第一个错误到这里时,把还没回来的请求全部取消
throw err
}
}所有请求共用同一个 controller.signal。任意一个失败,Promise.all 拒绝,进入 catch,调用一次 abort(),其他还在路上的 fetch 就会以 AbortError 拒绝,浏览器也会断开这些连接。这些 AbortError 同样会被 Promise.all 忽略,不会覆盖最初那个错误。
注意 fetch 只有网络失败才会 reject,HTTP 500 这类响应是正常兑现的,所以要自己判断 res.ok 并抛错,否则快速失败根本不会触发。
容易答错的地方
- 以为「第一个」指数组里的第一项
Promise.all拿到的是时间上最先 reject 的那个错误,和它在数组里排第几无关。上面的例子里 B 排第二,但它最先失败,拿到的就是 B 的错误。- 以为
Promise.all拒绝后,其他任务就停了 - Promise 本身没有取消的能力。任务在创建 Promise 时就开始了,
Promise.all拒绝只是不再等待,其他任务会完整执行,包括它们的副作用,比如写数据库、发埋点。 - 以为能拿到所有失败的原因
Promise.all只给出第一个错误,后面的全部丢弃。要知道所有失败原因,改用Promise.allSettled,它会等所有任务结束,把每一项的结果都告诉你。
面试官还会怎么问?
如果想等全部结束再统一报错,怎么写?
用 Promise.allSettled 等所有任务落定,再筛出 status === 'rejected' 的项。如果有失败,可以把所有 reason 收集起来,抛一个 AggregateError 出去,调用方就能一次看到全部错误。
手写 Promise.all 时,快速失败怎么实现?
每一项都 Promise.resolve(item).then(onFulfilled, reject),把外层 Promise 的 reject 直接作为每一项的失败回调。第一个失败的调用 reject 让外层落定,后面再调用 reject 什么也不会发生,因为 Promise 的状态只能改变一次。所以快速失败和「后续错误被忽略」是同一个机制带来的。
await 多个请求时,一个一个写 await 会有同样的问题吗?
会有另一种问题。const a = await p1; const b = await p2 如果 p2 先 reject,而此时代码还卡在 await p1,这时 p2 的拒绝暂时没有处理函数,可能触发未处理拒绝的警告。Promise.all 一次性给所有项挂上处理函数,就没有这个问题。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。