先记住这个答案
Promise.any 只要有一个输入成功就兑现为那个值,只有全部失败时才拒绝。拒绝值是一个 AggregateError,它继承自 Error,多了一个 errors 属性,是一个数组,按输入顺序存着每个 Promise 的拒绝原因。所以在 catch 里先判断 err instanceof AggregateError,再遍历 err.errors,就能把每个失败原因都拿出来。
- 全部失败才拒绝,拒绝值是
AggregateError - 失败原因在
err.errors,顺序和输入一致 - 传空数组会立刻以
AggregateError拒绝 - 判断用
err instanceof AggregateError
三个节点全挂了会拿到什么
典型场景是多节点兜底:同一份数据有主节点、备用节点和 CDN 三个来源,谁先成功就用谁。如果三个都失败,就得告诉用户或者上报每个节点的失败原因。
const p1 = Promise.reject(new Error('主节点超时'))
const p2 = new Promise((_, reject) => setTimeout(() => reject(new Error('备用节点 502')), 100))
const p3 = Promise.reject('CDN 不可用') // 拒绝原因不一定是 Error 对象
Promise.any([p2, p1, p3]).catch((err) => {
console.log(err instanceof AggregateError) // true
console.log(err.name)
console.log(err.errors.length)
// p2 最后失败,但它排在第 0 位
err.errors.forEach((e, i) => console.log(i, e instanceof Error ? e.message : e))
})查看输出与解释
true
AggregateError
3
0 备用节点 502
1 主节点超时
2 CDN 不可用p1 和 p3 立刻就失败了,p2 要等 100 毫秒才失败。Promise.any 会一直等到最后一个也失败才拒绝,所以这里的 catch 在 100 毫秒后才执行。
打印出来先是 p2,然后才是 p1 和 p3,也就是传进去的顺序,和谁先失败无关。这和 Promise.all 的结果数组是同一个规则:按位置对应,方便你知道第几个来源出了问题。
AggregateError 本身长什么样
AggregateError 是 ES2021 和 Promise.any 一起加进来的内置错误类型,专门用来表示「好几个错误打包成一个」。
- 它有哪些属性
name固定是'AggregateError'。message由引擎生成,V8 里是All promises were rejected,不要拿它做判断。errors是真正有用的部分,里面的元素就是原样的拒绝原因:原来reject的是Error对象,这里就是Error对象;原来reject的是字符串,这里就是字符串。所以遍历时最好像上面代码一样兼容两种情况。 它也可以自己用new创建,写法是new AggregateError([err1, err2], '批量上传失败'),第一个参数是错误列表,第二个是message。自己写批量任务,需要把多个失败一起抛出去时,可以直接复用这个类型。- 空数组是个特例
Promise.any([])没有任何输入可以成功,所以它会直接以AggregateError拒绝,errors是空数组。这一点和Promise.all([])正好相反,后者会立刻兑现为空数组。动态拼接请求列表时,如果列表可能为空,要先判断一下,别让用户看到一个莫名其妙的失败提示。
容易答错的地方
- 以为有一个失败就会进
catch - 那是
Promise.all的行为。Promise.any会忽略中间的失败,只要最后有一个成功,then就能拿到值,那些失败原因不会出现在任何地方。只有全军覆没,才会进catch。 - 以为
errors按失败时间排序 errors按输入位置排列。想知道哪个来源失败了,直接按下标对应原数组就行,不需要额外记录。- 在
catch里直接读err.message给用户看 AggregateError的message是引擎写的笼统文案,看不出具体原因。要给用户或日志有用的信息,得去err.errors里取。
面试官还会怎么问?
catch 里怎么区分 AggregateError 和别的错误?
用 err instanceof AggregateError。Promise.any 自己只会以 AggregateError 拒绝,但如果你在它后面接了 then,then 回调里抛出的普通错误也会流到同一个 catch,所以判断一下类型更稳。
想要全部结果,不管成功失败,该用什么?
用 Promise.allSettled。它等所有输入都结束,返回一个数组,每项是 { status: 'fulfilled', value } 或 { status: 'rejected', reason },永远不会拒绝。Promise.any 适合「要一个成功的就够」,Promise.allSettled 适合「每个结果都要处理」。
老浏览器不支持 Promise.any 怎么办?
可以用 Promise.all 反过来实现:把每个 Promise 的成功和失败对调,成功变拒绝、失败变兑现,再交给 Promise.all。这样第一个成功会让 Promise.all 立刻拒绝,拿到的就是那个成功值;全部失败时 Promise.all 兑现,拿到的正好是按顺序排列的错误数组。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。