跳转至

多路并发与错误管理 — all / race / allSettled / any 的分工

这一章回答两个问题: 手上有一叠票(多个 Promise)时,语言给了哪几种"收票"方式,各自什么时候用; 以及票被拒了之后,怎么管才不漏、不吞、不崩。 读完你能为"等全部 / 等最快 / 个个交代 / 第一个成功"四种需求各写出一句代码,并搭起全局错误兜底。

1. 这一章讲什么

第 03 章处理"一张票的一生",这一章处理"一叠票"——真实应用几乎都是同时挂着好几个异步操作:仪表板要三个接口都到才渲染,超时要和请求赛跑,批处理要每个结果都存档。结构:

  1. 四个静态方法的分工表与主线推演(第 3 节——核心);
  2. 拒绝管理的五条纪律(第 4 节);
  3. DevTools 里看 Promise 的具体按法(第 5 节)。

2. 顶层全景:四种"收票"语义一张表

原书四个方法的语义,先压成一张表再逐个展开1:

方法 什么时候落定 失败语义 一句话场景
Promise.all 全部兑现 任何一个拒,立刻整体拒(短路) 仪表板齐了才渲染
Promise.race 第一个落定(成或败都算) 随第一个 请求 vs 超时
Promise.allSettled(ES2020) 全部落定(成败都算) 永不因单个失败而拒;给每个一份交代 批处理,个个入账
Promise.any(ES2021) 第一个兑现 全拒才拒,附全部原因 多服务器择优

3. 核心原理:主线例子走四遍

本章主线例子是一个仪表板,要同时取三类数据(耗时是模拟值):用户资料 1 秒、帖子 1.5 秒、好友 2 秒。

  t=0      三个请求同时发出
  t=1.0s   用户资料到了 ─┐
  t=1.5s   帖子到了     ├─ Promise.all 等到 t=2.0s 最后一张,然后一次交付
  t=2.0s   好友到了   ──┘

3.1 Promise.all:齐了才收,一拒全拒

Promise.all([fetchUserData(), fetchUserPosts(), fetchUserFriends()])
  .then(([userData, userPosts, userFriends]) => { /* 更新仪表板 */ })
  .catch(error => console.error('An error occurred:', error));

两个要点。其一,结果按输入数组的顺序交付,不是按到达顺序——即使好友最后到,它仍在结果的第三位2。其二,短路:任何一个拒,all 立刻整体拒,不等剩下的3。天气主线(第 00 章)三个接口绑一个 Promise.all 就是这个语义:少任何一个,页面就不拼。

3.2 Promise.race:谁先到听谁的

把 3.1 的请求换成"数据和计时炸弹赛跑"(数据 3 秒到,超时 2 秒引爆):

const fetchData = new Promise(resolve => setTimeout(() => resolve('Data fetched successfully'), 3000));
const timeout   = new Promise((_, reject) => setTimeout(() => reject(new Error('Request timed out')), 2000));

Promise.race([fetchData, timeout])
  .then(result => console.log(result))
  .catch(error => console.error(error.message));   // 实际输出:Request timed out

输出是超时错误——因为 2 秒 < 3 秒,炸弹先落定,race 随它4。race 不关心结果好坏,只认时间;请求方赢了同理。这就是实现"请求限时"的标准配方,后面第 13 章的速查表还收着它的变体。

3.3 Promise.allSettled:批处理的记账员

allSettled(ES2020 加入)等每一个都落定,且永远兑现——交付的是一份"判决书"数组,每项长这样5:

{ status: 'fulfilled', value: ... }   // 或
{ status: 'rejected',  reason: ... }

主线例子换成批处理视角:三个任务一个成功、一个失败、一个慢成功,allSettled 给的交代是"1 兑现(值 1)、2 拒绝(原因 Error occurred)、3 兑现(值 3)"——一个不落6。它和 all 的差别就是失败语义:all 是"一票否决",allSettled 是"个个入账,事后分诊"。选哪个,取决于业务上"部分失败还算不算数"。

3.4 Promise.any:多路择优,全灭才报错

any(ES2021)只认成功:三个服务器,1 号 1 秒拒绝、2 号 2 秒成功、3 号 3 秒成功——any 交付 2 号的结果,1 号的失败被忽略7:

Promise.any([server1, server2, server3])
  .then(result => console.log('First successful response:', result))
  .catch(error => {
    if (error instanceof AggregateError) console.error('All servers failed:', error.errors);
  });

只有全部拒绝时它才拒绝,且错误是 AggregateError(一种把多个错误打包的错误),.errors 里存着全部拒因7。

判断(我们的,不是书里的): 这四个方法其实是同一张二维表的四个格子——横轴"等全部还是等第一个",纵轴"失败算数还是不算数"。学的时候别背四个 API,背这张表;写代码时先问业务"要不要部分成功",答案直接指向其中一个格子。 如果错,会错在: 语义交叉处有暗角——race 会被先拒绝的票带崩(不是"第一个成功"),这点和 any 正相反,死记表格的人会在这里翻车;表格本身没错,错的是把 race 记成"取第一个成功"。

3.5 版本坐标

allSettled 是 ES2020、any 是 ES2021 的标准,老环境(或老教程)没有它们,常见替代是手写计数器或退而用 race 链1。书按 2025 年的口径直接教,合理。

4. 错误管理:五条纪律

原书给了一组实战纪律,拼起来就是一套"拒绝管理"流程8:

  1. 链尾必挂 catch。 任何 .then 链都以 catch 收尾,防未处理的拒绝(未处理的拒绝在浏览器里是静默失踪,在 Node.js 里会直接警告甚至终止进程);
  2. 要传就再抛。 catch 里处理完(比如把错误记下来、清理)还想让上层知道,就 throw error——错误继续沿链传;catch 里不抛而 return 值,等于宣布"到此恢复"(第 03 章 5.4 的语义);
  3. finally 只做清理。 成败都要做的收尾(关连接、关加载动画)放 finally,别在里面改流程;
  4. 自定义错误类型分诊。 造 NetworkError、ValidationError 这样的子类,catch 里用 instanceof 分流处理——错误类型成了 API 的一部分;
  5. 全局兜底。 局部 catch 是防线,不是全部;浏览器端挂 window.addEventListener('unhandledrejection', …),Node.js 端挂 process.on('unhandledRejection', …),兜住所有漏网之鱼9。
  一条链上的错误防线:
  步骤.then.then ── 局部 catch(分诊/恢复)── throw ── 上层 catch ──…── 全局 unhandledrejection(兜底)

5. 调试 Promise:DevTools 按法

原书给了一组 Chrome DevTools 的具体操作10,对应"Promise 看不见、摸不着"的痛点:

操作 干什么
异步调用栈 Sources 面板断下来后,展开调用栈能看到跨 await 的完整来路——异步代码也能"从下往上看"
黑盒脚本(Blackboxing) 把 node_modules 标成黑盒,单步调试时不陷进库代码
步进 await 断在 async 函数里,Step 会自动跳到下一个相关行,像在调同步代码
Console 里看票 console.log(myPromise) 直接打印 Promise 对象,状态(待定/兑现/拒绝)与值一目了然
console.trace() 插在链中间,打印"此刻是谁把我调起来的"
条件断点 / logpoint 断点可以带条件(只在 data.status === 'error' 时停),logpoint 不用改代码就能打点
Network 面板 涉网请求看时序、状态码、响应体,和断点配合定位

3.6 四方法的输入输出形状

选方法时最容易记混的是"喂进去什么、吐出来什么"。把四个方法的输入输出摆成一张表(数组内容是为演示编的)11:

方法 喂进去 全部成功时吐出 失败时吐出
Promise.all [票A, 票B, 票C] A值, B值, C值 第一个拒因(其余票不再等)
Promise.race [票A, 票B] 第一个落定的值 若先落定的是拒绝,就是那个拒因
Promise.allSettled [票A, 票B, 票C] [{status:'fulfilled',value},…] 每项一判决 永远不拒;失败项是
Promise.any [票A, 票B, 票C] 第一个成功的值 AggregateError(.errors 是全部拒因数组)

两行使用提醒。其一,all 与 race 喂非票的普通值也合法——语言会先把普通值包成已兑现的票,所以 Promise.all([fetch(), 'fallback']) 这种混装能跑,但最好不要依赖,读代码的人会困惑。其二,race 之后输家并没有消失:超时的那个计时器到点照样触发,只是没人听了——第 13 章 withTimeout 的"哑弹"问题,根子就在 race 只"宣布胜负",不"终止比赛"。要真终止,用第 09 章的 abort。

3.7 主线例子跑 allSettled:批处理视角的完整走查

把主线例子的三请求改成"批处理"场景走一遍:任务1成功(值1)、任务2失败(拒因 Error)、任务3慢成功(值3),三个都是独立的记账条目12:

  t=0    三个任务同时发出
  t=0.1  任务1 落定(兑现)── 不报告,继续等
  t=0.2  任务2 落定(拒绝)── 不报告,继续等   ← all 在这里就已经整体拒了,这是关键差别
  t=1.0  任务3 落定(兑现)── 全员到齐,交付:
         [
           { status: 'fulfilled', value: 1 },
           { status: 'rejected',  reason: Error('Error occurred') },
           { status: 'fulfilled', value: 3 }
         ]

交付顺序仍按输入顺序,与落定先后无关。消费端的标准写法是对每项分诊:

results.forEach((result, index) => {
  if (result.status === 'fulfilled') console.log(`任务${index+1} 成功:`, result.value);
  else console.log(`任务${index+1} 失败:`, result.reason);
});

选择口诀再压一遍:"要不要部分结果?"要——allSettled;不要(一个坏全算坏)——all;只要一个——any;比谁快——race。

4.1 未处理拒绝在两个环境的真实表现

纪律一说的"未处理拒绝",在两个宿主里表现不同,值得各自看清13:

浏览器:拒绝没有 catch 接,控制台打一条警告(Chrome 是 Uncaught (in promise)),页面照常跑——错误就这么无声地没了。这是前端最阴险的失败模式:一切看起来正常,只是数据没来。

Node.js:未处理拒绝触发 unhandledRejection 事件;老版本(15 之前)默认只打警告,新版本默认策略趋向于直接终止进程——一个没人接的拒绝能杀掉整个服务。

两边的共同解法就是纪律五的全局兜底:

// 浏览器:兜底 + 上报
window.addEventListener('unhandledrejection', event => {
  console.error('Unhandled rejection:', event.promise, event.reason);
  // 这里接上报(第 07 章 5.3)
});
// Node.js
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});

要留意这两个兜底的定位:它们是安全气囊,不是座位。正常流程不该依赖它们——每条链仍要挂自己的 catch(能恢复的就地恢复),全局兜底只兜"漏网的"(通常是编程遗漏),并且它拦下了不等于处理完了,多数实现里兜底之后进程仍可能退出。

4.2 自定义错误的分诊走查

纪律四的 NetworkError/ValidationError 例子,把分诊流程完整走一遍14:

class NetworkError extends Error {      // 造一个"网络错误"的种类
  constructor(message) { super(message); this.name = 'NetworkError'; }
}
class ValidationError extends Error {   // 造一个"校验错误"的种类
  constructor(message) { super(message); this.name = 'ValidationError'; }
}

fetchData()
  .then(validateData)      // 数据不对就 throw new ValidationError(...)
  .then(processData)
  .catch(error => {
    if (error instanceof NetworkError)      retryOrFallback();
    else if (error instanceof ValidationError) showFixForm();
    else throw error;                        // 不认识,继续往上递
  });

执行时序:fetch 挂了 → fetchData 的票拒绝,错误是 NetworkError → catch 第一个分支接住,走重试或回退;数据到了但内容不对 → validateData 抛出 ValidationError → 第二个分支接住,引导用户改表单;其他意外 → 原样再抛,交给上层。三个分支各管一类,错误类型成了代码的一部分接口——消费端按类型分诊,比按错误文本猜(文本会改、会本地化)可靠得多。第 07 章的错误上报也会受益:上报的错误带类型,统计看板能按类聚合。

4.3 调试清单的实战组合

5 节的 DevTools 手段单独看都是零件,真实排障是一套组合拳。以"仪表板数据偶尔显示不全"为例,原书手段的组合用法15:

  1. 先在链中间插 console.trace()——不猜,先看执行路径是否真的走了每一个 then; trace 会打印"此刻是谁调起来的",一眼看出哪一环被跳过;
  2. 断点打在被跳过的 then 里 + 打开 Async 栈——看它的上游,确认是上游没 return(第 03 章 5.2 的忘记 return)还是上游拒绝了;
  3. Console 打印那张可疑的票——状态是 fulfilled 却值不对?那是上游 resolve 递错了东西;
  4. 涉网就切 Network 面板——请求发出去了吗?响应 200 吗?时序里它排在哪?配合断点,能把"数据没到"和"到了没用"分开——前者是网络问题,后者是代码问题,修法完全不同;
  5. 重放——Replay XHR 单独重发那个请求,排除"偶发网络抖动",确认问题是否可复现。

这套组合拳的顺序原则:先确认事实(路径、状态、网络),再动代码。异步 bug 的误诊率极高,因为每个观察者(打印输出、断点)本身就改变时序——先事实、后假设,能少绕一半弯路。

3.8 四个方法的两个共同前提

收尾两个容易忽略的前提16:

其一,输入是"票的数组",不是"取数函数的数组"。 Promise.all([fetchUserData, fetchUserPosts])——传的是函数本身,all 会把函数当值包成票,立刻全部兑现,结果是一堆函数,不是数据。正确姿势是先调用:Promise.all([fetchUserData(), fetchUserPosts()])——括号的有无,就是"发出请求"与"提起函数"的差别。第 08 章错误三(忘 resolve)和这里互为镜像:那边忘了递出,这边忘了发起。

其二,这些方法不启动任何东西,只组织已经在飞的票。 票在你写进数组的那一刻就已经在途;all/race/any 做的是"收票规则",不是"发令枪"。理解这一点,就理解了为什么"并行要显式说"——显式的部分不在 all,在数组里那几次调用。

6. 作者的判断与证据

本章的判断集中在"默认值"上:catch 总是挂上、finally 总是收尾、全局兜底总是配——作者把这些从"可选优化"升格为"默认动作"8。证据依旧是可复现的小代码:3.2 的超时例子谁跑都是 Request timed out。

版本取舍上,allSettled/any 被放在 all/race 之后讲,且明写 ES2020/ES2021 的来历——作者显然希望读者按"新代码直接用新的"来学,而不是先学 all/race 再被迫迁移1。

7. 边界与局限

  • AggregateError 只在 Promise.any 全灭时出现,其他组合的一堆拒因没有标准收集方式;
  • 调试一节以 Chrome 为准,Firefox/Safari 的对应面板名字不同,步骤要另查;
  • 没有讲 Promise.all 的"部分结果回收"(拒了之后其余票的去向),也没有取消语义——取消要等第 09 章的 AbortController。

8. 可带走的

  1. 四格表背下来:等全部/等第一个 × 失败算数/不算数 → all / allSettled / race / any;
  2. Promise.all 按输入顺序交付,不按到达顺序;一拒立即整体拒(短路);
  3. Promise.race 随"第一个落定"走,先拒绝也会带崩它——想做"限时"就让它和超时器赛跑;
  4. allSettled 给每张票一份 {status, value/reason} 判决书,批处理用它;
  5. Promise.any 只收第一个成功,全灭抛 AggregateError(里面 .errors 是全部拒因);
  6. 拒绝管理五纪律:链尾 catch、要传就再抛、finally 只清理、错误类型分诊、全局兜底;
  7. 调试异步看两样:异步调用栈(跨 await 的来路)、Network 面板(涉网时序)。

9. 原文地图

主题 原书章 原文位置
all 与仪表板例子 Chapter 4: Advanced Promises text/07-ch04-chapter-4-advanced-promises.txt:13(搜「dashboard」) · text/07-ch04-chapter-4-advanced-promises.txt:21(搜「Promise.all」)
all 的短路拒绝 同上 text/07-ch04-chapter-4-advanced-promises.txt:32(搜「short-circuiting」)
race 与超时例子 同上 text/07-ch04-chapter-4-advanced-promises.txt:38(搜「timeout」)
allSettled(ES2020) 同上 text/07-ch04-chapter-4-advanced-promises.txt:62(搜「ES2020」) · text/07-ch04-chapter-4-advanced-promises.txt:72(搜「allSettled」)
any 与 AggregateError 同上 text/07-ch04-chapter-4-advanced-promises.txt:93(搜「ES2021」) · text/07-ch04-chapter-4-advanced-promises.txt:109(搜「AggregateError」)
五条错误纪律 同上 text/07-ch04-chapter-4-advanced-promises.txt:119(搜「Always chain」) · text/07-ch04-chapter-4-advanced-promises.txt:131(搜「Rethrowing」) · text/07-ch04-chapter-4-advanced-promises.txt:168(搜「NetworkError」)
全局兜底 同上 text/07-ch04-chapter-4-advanced-promises.txt:201(搜「unhandledRejection」) · text/07-ch04-chapter-4-advanced-promises.txt:208(搜「unhandledrejection」)
DevTools 调试 同上 text/07-ch04-chapter-4-advanced-promises.txt:220(搜「Async Stack Traces」) · text/07-ch04-chapter-4-advanced-promises.txt:232(搜「Blackboxing」)

本章四个静态方法是全书使用频率最高的 API,值得把 3.6 的输入输出形状表抄进自己的工作笔记;而再往后,第 09 章的 AbortController 会给"race 假装取消"补上真正的取消,四个方法的分工表到那时还要再加一列。

至此,"一叠票"的收法齐了;下一章解决另一个方向的痛:把已经会的 Promise 写法,读回成人话。 (本章完:收票四法与拒绝管理五纪律,是全书实际开发中被查阅频率最高的一章,建议加进浏览器收藏夹。)


  1. 出处:「Chapter 4: Advanced Promises」第 9-19 段(text/07-ch04-chapter-4-advanced-promises.txt:15,搜「iterable」)。Promise.all 取一个可迭代对象(通常是数组)返回新 Promise;allSettled 见第 62 段(搜「ES2020」),any 见第 93 段(搜「ES2021」)。 ↩↩↩

  2. 出处:同章第 21-30 段(text/07-ch04-chapter-4-advanced-promises.txt:30,搜「destructure」)。三个接口响应时间不同(1000/1500/2000 毫秒),结果数组按序解构为 [userData, userPosts, userFriends]。 ↩

  3. 出处:同章第 32 段(text/07-ch04-chapter-4-advanced-promises.txt:32,搜「short-circuiting」)。原文:"if any promise in the array rejects, the entire Promise.all rejects immediately with that error, short-circuiting the process." ↩

  4. 出处:同章第 40-54 段(text/07-ch04-chapter-4-advanced-promises.txt:48,搜「Promise.race」)。数据 3000 毫秒、超时 2000 毫秒,实际输出超时错误。 ↩

  5. 出处:同章第 74-81 段(text/07-ch04-chapter-4-advanced-promises.txt:75,搜「fulfilled」)。每项结果含 status 与 value/reason 两个字段。 ↩

  6. 出处:同章第 66-89 段(text/07-ch04-chapter-4-advanced-promises.txt:85,搜「Promise 1 fulfilled」)。批处理例子:一成功一拒绝一慢成功,输出逐条交代。 ↩

  7. 出处:同章第 95-111 段(text/07-ch04-chapter-4-advanced-promises.txt:101,搜「Promise.any」;:109,搜「AggregateError」)。server1 拒绝而 any 仍交付 server2 的结果;全拒时拒绝并携带 AggregateError。 ↩↩

  8. 出处:同章第 115-212 段(text/07-ch04-chapter-4-advanced-promises.txt:119,搜「Always chain」;:135,搜「Rethrowing」;:152,搜「cleanup」;:166,搜「Custom Error Types」)。四条纪律依次位于这些段。 ↩↩

  9. 出处:同章第 195-212 段(text/07-ch04-chapter-4-advanced-promises.txt:201,搜「unhandledRejection」;:208,搜「unhandledrejection」)。Node.js 与浏览器两种全局监听照原书。 ↩

  10. 出处:同章第 214-301 段(text/07-ch04-chapter-4-advanced-promises.txt:220,搜「Async Stack Traces」;:234,搜「Blackboxing」;:246,搜「Step」;:258,搜「console.log」;:271,搜「console.trace」;:291,搜「Network Tab」)。 ↩

  11. 出处:「Chapter 4: Advanced Promises」第 15-111 段(text/07-ch04-chapter-4-advanced-promises.txt:15,搜「iterable」;:62,搜「ES2020」;:93,搜「ES2021」)。四方法的语义与输出结构照原书。 ↩

  12. 出处:「Chapter 4: Advanced Promises」第 66-89 段(text/07-ch04-chapter-4-advanced-promises.txt:72,搜「allSettled」;:83,搜「Promise 1 fulfilled」)。批处理例子与逐条输出的消费代码照原书。 ↩

  13. 出处:「Chapter 4: Advanced Promises」第 195-212 段(text/07-ch04-chapter-4-advanced-promises.txt:201,搜「unhandledRejection」;:208,搜「unhandledrejection」)。两种环境的监听与 "act as a safety net" 照原书。 ↩

  14. 出处:「Chapter 4: Advanced Promises」第 164-193 段(text/07-ch04-chapter-4-advanced-promises.txt:168,搜「NetworkError」)。两个自定义错误类与 instanceof 分诊照原书。 ↩

  15. 出处:「Chapter 4: Advanced Promises」第 214-301 段(text/07-ch04-chapter-4-advanced-promises.txt:273,搜「console.trace」;:291,搜「Network Tab」)。console.trace、Network 面板等手段的组合使用是对原书清单的应用示范。 ↩

  16. 出处:「Chapter 4: Advanced Promises」第 15-19 段(text/07-ch04-chapter-4-advanced-promises.txt:21,搜「fetchUserData()」)。原书例子均以【调用后的票】入数组(fetchUserData() 带括号)。 ↩