DrillLab
第 06 / 09 节LESSON 06 / 09约 15 分钟~15 min

异步:Promise、await、all 和 allSettledAsync: Promise, await, all and allSettled

Q2 整道题就是异步,GraphQL resolver 每一个都是 async。这一节把它们讲透。All of Q2 is async work, and every GraphQL resolver is async. This lesson covers all of it.

3 个练习3 exercises地基 · 第 2 部分Foundations · Part 2
这一页有什么On this page7
学完这节你会After this lesson you can
  • 说清 Promise 的三种状态,以及 await 到底在等什么Explain the three states of a Promise, and what await is actually waiting for
  • 分清 Promise.all 和 Promise.allSettled 的行为差别Tell apart how Promise.all and Promise.allSettled behave
  • 知道「函数」和「函数的返回值」在异步里为什么必须分清Know why a function and the value a function returns must be kept apart in async code
  • 会用 try/catch 包住 awaitWrap an await in try/catch
这在考试里考什么What the exam does with this

Q2 要你手写一个「allSettled + 并发上限」;Federation 的每个 resolver 都是 async 且要求 try/catch。这一节是两道题共同的地基。Q2 asks you to write allSettled behaviour with a limit on how many run at once. In Federation, every resolver is async and has to use try/catch. This lesson is the base both questions stand on.

这节课要看的真实文件Real files this lesson looks at2 项 · 2 个可以展开看原文2 items · 2 can be opened
react-notes-app/q2/taskRunner.tsQ2 的题面与要求都在文件顶部注释里The Q2 problem and its requirements are in the comment at the top of the file
TypeScripttaskRunner.ts源项目From source
1// Q2: Implement a custom asynchronous task runner.
2//
3// Requirements:
4// 1. `tasks` is an array of FUNCTIONS. Each function, when called,
5// starts an async job and returns a Promise.
6// 2. At most `limit` tasks may be RUNNING at the same time.
7// A new task may only start after one of the running tasks finishes.
8// 3. The runner NEVER throws, even if some tasks reject.
9// It resolves with an array of results IN THE SAME ORDER as `tasks`:
10// { status: "fulfilled", value: T } for tasks that succeeded
11// { status: "rejected", reason: unknown } for tasks that failed
12// (This mimics Promise.allSettled, but with a concurrency throttle.)
13
14export type Task<T> = () => Promise<T>;
15
16export type SettledResult<T> =
17 | { status: "fulfilled"; value: T }
18 | { status: "rejected"; reason: unknown };
19
20export async function runTasks<T>(
21 tasks: Task<T>[],
22 limit: number,
23): Promise<SettledResult<T>[]> {
24 // TODO: implement me
25 const results: SettledResult<T>[] = new Array(tasks.length);
26
27 let nextIndex = 0;
28
29 const worker = async () => {
30 while (nextIndex < tasks.length) {
31 const i = nextIndex;
32 nextIndex++;
33
34 try {
35 const value = await tasks[i]();
36 results[i] = { status: "fulfilled", value };
37 } catch (reason) {
38 results[i] = { status: "rejected", reason };
39 }
40 }
41 };
42 if (tasks.length === 0) return [];
43
44 const workerCount = Math.min(limit, tasks.length);
45 const workers: Promise<void>[] = [];
46 for (let w = 0; w < workerCount; w++) {
47 workers.push(worker());
48 }
49 await Promise.all(workers);
50 return results;
51}
Source: react-notes-app/q2/taskRunner.ts
react-notes-app/q2/demo.ts验证台:打印实时并发数A test bench that prints the live concurrency count
TypeScriptdemo.ts源项目From source
1// Test harness for Q2. Run with: npm run q2
2// It prints how many tasks are running at each moment, so you can
3// VERIFY the concurrency never exceeds the limit.
4
5import { runTasks, type Task } from "./taskRunner";
6
7let running = 0;
8
9const makeTask = (id: number, ms: number, shouldFail = false): Task<string> => {
10 return () =>
11 new Promise((resolve, reject) => {
12 running++;
13 console.log(`task ${id} START (running now: ${running})`);
14 setTimeout(() => {
15 running--;
16 if (shouldFail) {
17 console.log(`task ${id} FAIL (running now: ${running})`);
18 reject(new Error(`task ${id} failed`));
19 } else {
20 console.log(`task ${id} DONE (running now: ${running})`);
21 resolve(`result of task ${id}`);
22 }
23 }, ms);
24 });
25};
26
27const tasks = [
28 makeTask(1, 300),
29 makeTask(2, 100),
30 makeTask(3, 200, true), // this one rejects
31 makeTask(4, 100),
32 makeTask(5, 150),
33 makeTask(6, 100),
34];
35
36runTasks(tasks, 2).then((results) => {
37 console.log("\n=== FINAL RESULTS (must be in original order) ===");
38 results.forEach((r, i) => console.log(`#${i + 1}`, r));
39});
Source: react-notes-app/q2/demo.ts
§01

Promise:一张「以后会给你结果」的凭据A Promise is a receipt for a result that arrives later

它有三种状态,而且只会变一次。It has three states, and it settles only once.

Promise 代表一件「现在还没完成、以后会有结果」的事。 三种状态:

  • pending —— 还在做
  • fulfilled —— 成功了,带一个值
  • rejected —— 失败了,带一个原因

一旦从 pending 变成后两者之一,就永久定下来,不会再变。

await 的作用是:「在这里等着,直到这个 Promise 定下来」。 成功就把值交给你,失败就抛出异常(所以要用try/catch 接)。await 只能写在async 函数里。

A Promise stands for something that is not finished yet but will have a result later. Three states:

  • pending — still working
  • fulfilled — succeeded, carries a value
  • rejected — failed, carries a reason

Once it moves out of pending into one of the other two, it is settled for good and never changes again.

What await does is: “stand here until this Promise settles”. On success it hands you the value; on failure it throws (which is why you catch it with try/catch). await only works inside an async function.

JavaScript真实项目里最常见的 async 长相The most common shape of async in a real project源项目From source
1async getOrdersByUserId(userId) {
2 await new Promise(resolve => setTimeout(resolve, 10)); // 假装网络延迟 10ms
3 return this.orders.filter(order => order.userId === userId);
4}
1async getOrdersByUserId(userId) {
2 await new Promise(resolve => setTimeout(resolve, 10)); // pretend a 10ms network delay
3 return this.orders.filter(order => order.userId === userId);
4}
Source: graphql-federation-practice/node-subgraph/src/dataSources/orderDataSource.js
async 函数的返回值一定被包成 Promise。所以哪怕这里 return 的是普通数组,调用方也得 await 才能拿到它。Whatever an async function returns is always wrapped in a Promise. So even though this one returns a plain array, the caller still has to await it to get the array.
§02

最关键的一个区分:函数,还是函数的返回值The key distinction: a function, or the value the function returns

Q2 整道题都建立在这个区分上。The whole of Q2 rests on this distinction.

看 Q2 的类型定义:

Task<T>一个函数: () => Promise<T>。 不是 Promise,是「调用之后才产生 Promise 的函数」。

这个区分为什么是整道题的关键?因为Promise 一旦创建就开始跑了, 没法暂停。如果 tasks 是一堆 Promise, 那它们在你拿到手之前就已经全部并发跑起来了,「并发上限」根本无从谈起。

正因为它们是函数,你才能控制「什么时候调用」—— 先调两个,等其中一个完了,再调第三个。这就是并发节流的全部原理。

Look at the Q2 type definitions:

Task<T> is a function: () => Promise<T>. Not a Promise — a function that only produces a Promise once you call it.

Why is that distinction the whole question? Because a Promise starts running the instant it is created, and there is no pause button. If tasks were a pile of Promises, they would all be running concurrently before you ever got your hands on them, and a concurrency cap would mean nothing.

Precisely because they are functions, you get to control when they are called — call two, wait for one to finish, then call the third. That is the entire principle behind concurrency throttling.

TypeScriptq2/taskRunner.ts(顶部类型定义)q2/taskRunner.ts (the type definitions at the top)源项目From source
1export type Task<T> = () => Promise<T>;
2
3export type SettledResult<T> =
4 | { status: "fulfilled"; value: T }
5 | { status: "rejected"; reason: unknown };
Source: react-notes-app/q2/taskRunner.ts
TypeScript示意Illustrative
1const task = () => fetch("/api/orders"); // 一个函数。什么都还没发生。
2const promise = task(); // 调用了 → 请求现在才发出去
3
4await task; // ✗ 错:在等一个函数,它不是 Promise,立刻就过去了
5await task(); // ✓ 对:先调用,再等它的返回值
1const task = () => fetch("/api/orders"); // a function. Nothing has happened yet.
2const promise = task(); // called → only now is the request sent
3
4await task; // ✗ wrong: waiting on a function, not a Promise, so it passes at once
5await task(); // ✓ right: call it first, then wait for what it returns
§03

Promise.all 和 Promise.allSettled:差别在「一个失败了怎么办」Promise.all and Promise.allSettled: they differ in what happens when one fails

Promise.allPromise.allSettled
全部成功返回值数组 [v1, v2]返回 [{status:"fulfilled",value}, ...]
有一个失败立刻整体 reject,其他结果全丢照样等全部结束,失败那个记成 rejected
顺序与输入一致与输入一致

两个都保证顺序与输入一致 —— 哪个先完成不影响结果数组的位置。 这一点很多人以为是「谁先完成谁在前」,是错的。

Q2 的要求原文是「The runner NEVER throws, even if some tasks reject」, 也就是 allSettled 的语义,再加一个并发上限。 而 Promise.all 在两个考试里也真实出现了 —— subgraph 的 DataLoader 批量函数就用它同时取多个订单。

Promise.allPromise.allSettled
All succeedAn array of values [v1, v2]Returns [{status:"fulfilled",value}, ...]
One failsRejects as a whole, immediately, and the other results are lostStill waits for all of them; the failed one is recorded as rejected
OrderMatches the inputMatches the input

Both of them guarantee the order matches the input — which one finishes first has no bearing on its slot in the result array. Plenty of people assume it is first-to-finish-comes-first. It is not.

The Q2 requirement reads “The runner NEVER throws, even if some tasks reject”, which is exactly allSettled semantics plus a concurrency cap. And Promise.all genuinely shows up in both exams too — the subgraph’s DataLoader batch function uses it to fetch several orders at once.

JavaScript真实项目里的 Promise.allPromise.all in a real project源项目From source
1function createShippingInfoLoader(shippingDataSource) {
2 return new DataLoader(async orderIds => {
3 console.log(`[DataLoader] Batching ${orderIds.length} shipping info requests`);
4
5 const shippingInfos = await Promise.all(
6 orderIds.map(id => shippingDataSource.getShippingInfo(id))
7 );
8
9 return shippingInfos;
10 });
11}
Source: graphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.js
map 把每个 id 变成一个 Promise,Promise.all 等它们全部完成。顺序与 orderIds 一致 —— 这对 DataLoader 是硬要求,因为它靠位置把结果发回给各个调用方。map turns each id into a Promise, and Promise.all waits for all of them to finish. The result order matches orderIds. DataLoader requires that, because it uses position to send each result back to the caller that asked for it.
§04

并发上限的实现思路:共享一个游标的 workerHow to limit how many run at once: workers sharing one cursor

别想复杂了。就是「开 limit 个工人,一起从同一个待办队列里抢活」。It is simpler than it sounds. Start limit workers, and let them all take the next job from the same queue.

想象 6 个任务,并发上限 2。错误的思路是「切成 3 批,每批 2 个」—— 那样每批都要等最慢的那个,浪费。

正确的思路是 worker pool(工人池):

  1. 准备一个共享游标 nextIndex = 0
  2. 启动 limit 个 worker,每个都是一个 async 函数。
  3. 每个 worker 循环:抢走当前游标指向的任务(游标 +1)→ 跑它 → 结果写回 results[i] → 回到循环开头再抢下一个。
  4. 队列空了,worker 自然退出。
  5. await Promise.all(workers) 等所有 worker 收工。

同时在跑的永远只有 limit 个 worker,所以并发数天然不会超。 结果按 results[i] 的下标写回,所以顺序自动是对的, 不需要额外排序。

实测这个思路在项目里的输出(npm run q2):running now 始终 ≤ 2,最终 6 条结果顺序与输入一致, 第 3 个任务以 rejected 出现。完整实现在 React 那门课的 Q2 那一节。

Picture 6 tasks with a concurrency cap of 2. The wrong idea is “cut it into 3 batches of 2” — every batch then waits on its slowest member, which is wasteful.

The right idea is a worker pool:

  1. Set up one shared cursor, nextIndex = 0.
  2. Start limit workers, each one an async function.
  3. Each worker loops: grab the task the cursor points at (cursor +1) → run it → write the outcome back into results[i] → back to the top of the loop to grab the next one.
  4. Queue empty, the worker exits on its own.
  5. await Promise.all(workers) waits for every worker to clock out.

Only limit workers are ever running at once, so the concurrency count cannot go over by construction. Results are written back at index results[i], so the order is right automatically and no extra sorting is needed.

Measured output of this approach in the project (npm run q2): running now never reaches 3, the final 6 results come out in input order, and task 3 shows up as rejected. The full implementation is in the Q2 lesson of the React course.

Textnpm run q2 的真实输出The real output of npm run q2已跑通Verified
1task 1 START (running now: 1)
2task 2 START (running now: 2) ← 到上限了,3 号只能等
3task 2 DONE (running now: 1)
4task 3 START (running now: 2) ← 有位置了,立刻补上
5task 1 DONE (running now: 1)
6task 4 START (running now: 2)
7task 3 FAIL (running now: 1) ← 失败也只是空出一个位置,不影响别人
8task 5 START (running now: 2)
9task 4 DONE (running now: 1)
10task 6 START (running now: 2)
11task 5 DONE (running now: 1)
12task 6 DONE (running now: 0)
1task 1 START (running now: 1)
2task 2 START (running now: 2) ← at the limit, task 3 must wait
3task 2 DONE (running now: 1)
4task 3 START (running now: 2) ← a slot opened, filled at once
5task 1 DONE (running now: 1)
6task 4 START (running now: 2)
7task 3 FAIL (running now: 1) ← a failure only frees a slot, others run on
8task 5 START (running now: 2)
9task 4 DONE (running now: 1)
10task 6 START (running now: 2)
11task 5 DONE (running now: 1)
12task 6 DONE (running now: 0)
读这段输出的方法:盯住 running now,它从来没到 3。而且任务 3 失败之后,4、5、6 照样跑完了 —— 这就是 allSettled 的语义。How to read this output: watch running now, which never reaches 3. And after task 3 fails, tasks 4, 5 and 6 still finish — that is what allSettled means.
§05

try/catch 包住 awaitWrapping await in try/catch

await 遇到 rejected 会抛异常, 行为和 throw 一样。所以要接住它,就用普通的try/catch:

Federation 那道题的每个 resolver 都要求这个结构 —— TODO 原文写的是「with proper error handling」。

When await meets a rejected Promise it throws, behaving exactly like throw. So to catch it, you use an ordinary try/catch:

Every resolver in the Federation question wants this shape — the TODO reads “with proper error handling”.

TypeScriptQ2 里的 try/catch(参考答案节选)try/catch in Q2 (excerpt of the reference answer)源项目From source
1try {
2 const value = await tasks[i]();
3 results[i] = { status: "fulfilled", value };
4} catch (reason) {
5 results[i] = { status: "rejected", reason };
6}
Source: react-notes-app/q2/taskRunner.ts
关键点:catch 之后 worker 没有退出,循环继续。所以一个任务失败不会连累其他任务 —— 这正是「NEVER throws」的实现方式。The key point: after the catch the worker does not exit, the loop keeps going. So one failed task does not affect the others. That is how the NEVER throws requirement is met.
练习Practice

动手做Get your hands on it

填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.

L1认出来Spot it该用 all 还是 allSettledall or allSettled

Q2 的要求原文:「The runner NEVER throws, even if some tasks reject. It resolves with an array of results IN THE SAME ORDER as tasks.」 这描述的是哪个内置方法的语义?

The exact wording of the Q2 requirement: “The runner NEVER throws, even if some tasks reject. It resolves with an array of results IN THE SAME ORDER as tasks.” Which built-in method behaves like that?

先选一个选项Pick an option first
L1认出来Spot it为什么 tasks 是「函数数组」而不是「Promise 数组」Why tasks is an array of functions, not an array of Promises

Task<T> = () => Promise<T>。 如果题目改成传一个 Promise<T>[] 进来, 会出什么问题?

Task<T> = () => Promise<T>. If the question passed in a Promise<T>[] instead, what would go wrong?

先选一个选项Pick an option first
L2填空Fill the blanks补全 DataLoader 的批量函数Fill in the batch function of the DataLoader

这是 subgraph 里真实的 DataLoader 批量加载函数。 两个空都关系到「异步 + 数组」的固定套路。

This is the real DataLoader batch function from the subgraph. Both blanks are part of the fixed pattern for async work over an array.

JSsrc/resolvers/orderResolvers.js2 个空2 blanks
1function createShippingInfoLoader(shippingDataSource) {
2 return new DataLoader(async orderIds => {
3 const shippingInfos = await Promise.(
4 orderIds.(id => shippingDataSource.getShippingInfo(id))
5 );
6
7 return shippingInfos;
8 });
9}
把 2 个空都填上才能检查(还差 2 个)Fill all 2 blanks to check (2 to go)
迁移Transfer

换一道题也能用Works on other problems too

考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.

「不管有没有失败都要拿到全部结果」You need every result, whether some failed or not
Promise.allSettled 的语义That is what Promise.allSettled does
「限制同时进行的数量」You must limit how many run at the same time
传函数数组 + worker pool 共享游标Pass an array of functions, and have a pool of workers share one cursor
「一批 id 换一批数据」Turn a list of ids into a list of records
map + Promise.all,长度与顺序不变map plus Promise.all. The length and the order stay the same
「proper error handling」出现在 TODO 里A TODO comment asks for proper error handling
try { await ... } catchtry { await ... } catch
这节的要点What to take away
  1. Promise 三态,只定一次;await 成功给值、失败抛异常。A Promise has three states and settles only once. await gives you the value on success and throws on failure.
  2. () => Promise<T> 是函数,Promise<T> 是已经在跑的事 —— 并发控制只能靠前者。() => Promise<T> is a function. Promise<T> is work that has already started. Only the first form lets you control how many run at once.
  3. all 一个失败就整体失败;allSettled 全等完再汇总。两者都保证顺序。all fails as a whole as soon as one fails. allSettled waits for all of them and then reports. Both keep the order.
  4. 并发上限 = 开 limit 个 worker 抢同一个游标,结果按下标写回自动保序。To cap how many run at once, start limit workers that share one cursor, and write each result back at its own index so the order is kept.
  5. await 要用 try/catch 接;catch 之后循环继续,才叫「不抛错」。Catch an await with try/catch. Only if the loop carries on after the catch does the function really not throw.

接下来What next

  1. 把这一节的练习做掉Do this lesson’s exercises3 个,就在这一页上面 —— 别攒着最后一起做3 of them, further up this page — do not save them for later
    回到练习 ↑Back up to them ↑
  2. 接着看下一节Continue to the next lessonESM:import / export 与那些莫名其妙的报错ESM: import / export, and the errors that look strange at first
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: 数组与对象:不可变更新三件套Arrays and objects: three ways to update without changing the original