跳转至

async/await — 把票换成直白的句子,错误换回 try/catch

这一章回答三个问题: async/await 两个记号各自改变了什么; 一段 Promise 代码机械地翻译成 async/await 的规则是什么; 以及"看起来同步"和"真的同步"差在哪——这个差别什么时候咬人。 读完你会翻译、会处理错误、会在需要并行时想起 Promise.all。

1. 这一章讲什么

第 03 章的 Promise 把异步编排理顺了,但代码里还是一列 .then,读的时候要在脑子里把"每个 then 的输入是上一个 then 的 return"持续转译成自然语言的"然后"。async/await 就是把这个转译做进语法里1。结构:

  1. 两个记号的语义(第 3 节);
  2. 机械翻译规则,拿一段真实代码前后对照(第 4 节——核心);
  3. 错误处理:try/catch 的分层与再抛出(第 5 节);
  4. 与 Promise.all 的组合(第 6 节)。

2. 顶层全景:同一台机器,换了一块仪表盘

先记住本章最重要的一句话,原书明写的:async/await 没有让代码同步执行,异步操作照样在后台跑;它只是让代码"看起来、读起来"像同步的2。

  Promise 写法:                      async/await 写法:
  fetch(url)                         const response = await fetch(url);
    .then(r => r.json())             const data = await response.json();
    .then(data => 用)                用(data)
  ── 逻辑流要沿着 .then 拐弯 ──        ── 逻辑流就是从上到下的直线 ──
  底下跑的,是同一套事件循环和同一批队列

3. 核心原理(一):两个记号,三条规则

3.1 async:这个函数永远返回 Promise

在函数声明前加 async,它的返回值就被自动包进一张票:哪怕你 return 一个普通值,调用方拿到的也是兑现了那个值的 Promise3:

async function fetchData() {
  const response = await fetch('/api/data');
  return await response.json();
}
// fetchData() 的调用方拿到的是 Promise,不是 data

3.2 await:停在这里,等这张票

await 后面跟一个 Promise;执行到这一行时,函数停在此处(不是线程停——整条流水线被让出去干别的,第 01 章的事件循环照常转),票落定后把值取出来,继续下一行3。它只能用在 async 函数里,这是语法规定。

3.3 三个必须一起记住的事实

  1. 停在 await 的是"这个函数":函数外的世界不受影响——这正是界面不冻的原因;
  2. await 一个已兑现的票也会让出执行——哪怕结果立刻可用,也会排到微任务队列,这一点在拼时序(第 01 章的输出题)时重要;
  3. async 函数自己抛错 / 内部 await 的票被拒 → 函数返回的那张票拒绝——于是错误可以像同步代码一样被 try/catch 接(下一节)。

4. 主线例子:机械翻译五步

原书拿一段真实的两连请求做对照(取用户资料 → 取该用户的帖子)4。Promise 版:

function fetchUserData(userId) {
  return fetch(`/api/users/${userId}`)
    .then(response => response.json())
    .then(userData => {
      return fetch(`/api/posts?userId=${userData.id}`);
    })
    .then(response => response.json())
    .then(posts => ({ user: userData, posts: posts }))
    .catch(error => { console.error('An error occurred:', error); throw error; });
}

async/await 版:

async function fetchUserData(userId) {
  try {
    const userResponse = await fetch(`/api/users/${userId}`);
    const userData = await userResponse.json();

    const postsResponse = await fetch(`/api/posts?userId=${userData.id}`);
    const posts = await postsResponse.json();

    return { user: userData, posts: posts };
  } catch (error) {
    console.error('An error occurred:', error);
    throw error;
  }
}

翻译规则逐条对(这是本章最该带走的东西):

步骤 Promise 写法 async/await 写法
1 .then(r => r.json()) const x = await … 后单独一行处理
2 嵌套 then(依赖上一步结果) 上一步存下来,下一行直接用——缩进消失
3 .catch(error => …) catch 块,同一位置
4 catch 里 throw error 继续传 catch 里 throw error,一字不改5
5 函数 return 链尾的 Promise 函数 return 值,async 自动包票

原书特别点名了改进所在:新增一步操作(比如两个请求之间插一段处理)只需要在直线中间插一行,而在 Promise 版里要动链的结构5——可改性是这次重写最实际的收益。

5. 核心原理(二):错误处理回到 try/catch

5.1 分层:内层处理后,想传就再抛

原书的骨架是两层:内层函数管"这件事失败时记下错误+清理",然后 throw error 把错误递给外层;外层再决定怎么对用户交代6:

async function fetchData(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);  // 4xx/5xx 也算失败
    }
    return await response.json();
  } catch (error) {
    console.error('An error occurred while fetching the data:', error);
    throw error;                        // 再抛出,交给调用方
  }
}
async function processData() {
  try {
    const data = await fetchData('/api/data');
    console.log('Data processed successfully:', data);
  } catch (error) {
    console.error('Error processing data:', error);   // 内层递上来的错误在这收
  }
}
processData();

两处细节值得记住:response.ok 检查——fetch 把 404/500 当作"成功响应"(网络通了),所以 HTTP 错误必须自己判,response.ok 为 false 就手动抛错;throw 与 reject 同构——async 函数里 throw 出去的东西,调用方 await 时会以 catch 的形式收到。

5.2 顶层兜底

最外层再包一层 try/catch,或挂全局 unhandledRejection(第 04 章 4 节的纪律在 async 世界原样成立):漏网的未处理拒绝会一路滑到那里7。

判断(我们的,不是书里的): async/await 真正的历史功绩,是把错误处理从"链上的特例"恢复成"代码的常态"。Promise 时代 catch 的位置决定语义(恢复点 vs 终点站),读者要记住两套规则;async 世界里所有错误都走同一条 try/catch 通道,心智负担从"图"降回"块"。这也是为什么新手代码库的异步部分在 ES2017 之后肉眼可见地变整齐。 如果错,会错在: 当一段代码的错误处理需求是"每一步各自不同的恢复动作"(而不是统一兜底)时,平铺的 try/catch 反而不如链式 catch 的位置语义贴切——这时该回到 Promise 链,而不是硬套 async/await。本条判断覆盖的是主流写法,不是全部。

6. 核心原理(三):并行要显式地说

await 是串行记号:写两行 await,第二行一定等第一行。回到第 00 章的天气主线(三个互不依赖的接口),async 版要这样写才是并行8:

async function getUserInfo(userId) {
  try {
    const [userData, userPosts, userComments] = await Promise.all([
      fetchUserData(userId),
      fetchUserPosts(userId),
      fetchUserComments(userId)
    ]);
    return { user: userData, posts: userPosts, comments: userComments };
  } catch (error) {
    console.error('An error occurred while fetching user info:', error);
    throw error;
  }
}

要点:Promise.all([...]) 的三个调用在数组里就已经全部发起——它们同时上路;await 等的只是"这一捆票"整体落定。任何一个拒,捆票即拒,进 catch。

若业务要"部分失败也继续",换成 Promise.allSettled,拿到逐张判决后再分诊(第 04 章 3.3)9:

const results = await Promise.allSettled([fetchUserData(userId), fetchUserPosts(userId), fetchUserComments(userId)]);
return results.map(r => r.status === 'fulfilled' ? r.value : null);

这一节是对第 00 章"独立请求要并行"的最终兑现,也是对 2 节那句"看起来同步"的校正:async/await 负责"读",并行的"行"还得 Promise.all 来画。

6.1 翻译之后还要人工复查的两处

机械翻译五规则覆盖九成情况,但有两处翻完要人工看一眼11:

第一处:并行段。 Promise 版里"先造票再 all"的写法(第 04 章 3.1),翻成 async/await 时如果只把 then 换成 await,会翻成三行顺序 await——语义从并行变成串行,总耗时直接相加。复查口诀:凡是原代码里有 all/race/allSettled/any 的,翻完检查这些调用还在不在;在,并行保住了;不在,你刚刚把人家的并行改没了。

第二处:链中间的 catch。 Promise 版链中间的 catch 是"恢复点"(第 03 章 5.4),翻成 try/catch 时如果整体包一个大 try,语义会 subtly 变化:链上第二个 catch 只兜后半段错误,而大 try 兜全程。要保留精确语义,就得在对应位置拆成两个 try 块——这也是第 05 章 5.1 说"链的位置语义偶尔更贴切"的具体所指。

6.2 async 函数的四种面孔

async 可以标在四种函数上,行为一致(都返回 Promise),本书里都出现了12:

形态 样子 本书哪里用到
声明 async function f() { ... } 全书主力
箭头 const f = async () => { ... } 立即执行的包装(见下)
表达式 const f = async function() { ... } 少见
方法 obj = { async m() { ... } } 第 06 章 RateLimiter 的 run/process

在 top-level await(ES2022,第 09 章)之前,模块顶层想"一加载就执行一段异步",标准 workaround 就是"立即执行的 async 箭头函数":

(async () => {
  const config = await loadConfig();
  startApp(config);
})();

这个 (async () => { ... })() 的写法第 06 章的天气例子用了、第 10 章的项目也用了——它读起来怪,作用只有一个:给 await 提供一个 async 的家。top-level await 进标准后,这个家可以拆了,但存量代码里还到处都是。

6.3 await 之后的代码,为什么不冻界面

3.1 说"停的是这个函数",这句还有一个值得展开的推论13。看这个函数被调用时的全景:

async function load() {
  const data = await fetchData();   // ← 停在这行
  render(data);                     // ← 数据到了才执行
}
button.addEventListener('click', load);

用户点击按钮 → load() 开始跑 → 执行到 await → 这个函数让出流水线 → 用户继续滚动页面、点别的按钮、看加载动画转 → 数据到了 → load 从断点处醒来,执行 render。全程流水线没空转:等待期间它在处理别的事件回调。这就是"非阻塞"在 await 身上的样子——它不是"这个函数在等",而是"这个函数睡着了,把流水线让给了全世界"。

反过来也成立:await 不会把别的代码冻住,但会把同一个函数里 await 后面的代码冻到数据来。所以一个函数里 await 太多(串了七八个请求)时,卡顿的不是界面,是这个功能本身的响应速度——性能问题换了个位置,并没有消失。这也是第 06 章"依赖判定"在 async 世界的版本:没有依赖的两步,别写成上下两行 await。

6.4 组合用的 two 个完整变体

第 6 节给了 all 与 allSettled 两个版本,把它们的行为差异压成一张表,选的时候查14:

场景 用哪个 行为
三个接口缺一不可(仪表板) Promise.all 全成才兑现;一个拒,整体拒,进 catch
三个接口各管各的(资料+帖子+评论) Promise.allSettled 个个落定才兑现;失败项自行填 null/默认值
一个能用的就行(多镜像源) Promise.any 第一个成功就兑现;全灭才拒(第 04 章)
只要最快的一个(请求 vs 超时) Promise.race 先落定者胜(第 04 章 3.2)

两个变体共用的写法要点:数组里先全部发起(fetchUserData(userId), fetchUserPosts(userId), ... 在数组字面量里就已经是"同时在路上"),await 只等这一捆整体落定;解构(const [a, b, c] = ...)按输入顺序拆包,不是按到达顺序。

还有一个细节:allSettled 版里 result.status === 'fulfilled' ? result.value : null 这行判断,是把"判决数组"翻译成"可用数据"的固定动作——失败项填 null 还是默认值、要不要顺带记一笔,是业务决定,但别直接把 allSettled 的结果当数据用,它的元素是判决书不是值,这是换 allSettled 之后最容易犯的新错。

6.5 顶层兜底的完整形状

5.2 的顶层结构值得给一个完整模板,因为它是每个 async 程序的"出生证明"15:

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  // 应用相关的记录、告警
});

async function main() {
  try {
    await processData();
  } catch (error) {
    console.error('Top-level error caught:', error);
    // 必要的清理与错误上报
  }
}

main();

三层各管一段:内层 try/catch 管"预期内的失败"(恢复、默认值、重试);外层(这里)管"没想到的失败"(记下来、上报、体面退出);全局 unhandledRejection 管"连外层都没兜住的"(通常是漏写了 catch——兜底的同时,它本身就是一张"哪里有漏"的清单,值得定期翻)。三层都安静,才算错误处理做完了。

6.6 翻译练习:自己试一次

学到这里,最好的检验是把第 02 章的回调版主线手工翻成 async 版(翻完再翻回来),对照三个检查点:错误是不是只有一个 catch 收了?并行的段还在吗?每一步的 await 是不是都有必要(没有依赖的相邻 await,就是能合并的信号)?翻译能力的本质,是对两种写法语义的逐点映射——第 05 章的机械规则背下来只是开始,亲手翻过一遍才算会。

7. 作者的判断与证据

本章的判断比较克制,主要有两条。一是"async 没有取代 Promise"——async 函数永远返回 Promise、await 只认 Promise,理解 Promise 仍是基本功10。二是错误处理"跟着熟悉的结构走"——try/catch 对从其他语言来的开发者更友好,这是它相对 .catch 的真实优势6。

证据仍是对照代码:同一功能两版并列,可读性差距即证据。

8. 边界与局限

  • 循环里逐个 await 的性能陷阱(for 里串行 N 次)本章没讲,要到第 08 章的错误清单里才作为"循环竞态"出现,第 11 章才有明确的反模式对照;
  • 没有讲 await 在顶层(模块外)的用法——那是 ES2022 的 top-level await,第 09 章;
  • for await…of 配合异步迭代器的写法同样留给第 09 章;
  • 并发上限(同时最多 N 个请求)这个真实高频需求,全书都没有专门模式,只有第 06 章的限流器沾边。

9. 可带走的

  1. async 函数永远返回 Promise;await 只在 async 函数里可用,意思是"停在这行等票";
  2. "看起来同步"≠"跑起来同步":停的是这个函数,流水线照常让别人用;
  3. 机械翻译五规则:then→独立一行、嵌套→拆开存、catch→catch 块、throw 原样、return 值自动包票;
  4. fetch 不认为 404 是失败——必须自己查 response.ok;
  5. try/catch 分层:内层处理+再抛,外层收底;漏网的全局 unhandledRejection 兜底;
  6. await 写几行就串几行;并行必须显式 Promise.all(部分失败用 allSettled);
  7. async/await 没取代 Promise——它站在 Promise 肩上。

10. 原文地图

主题 原书章 原文位置
语法与 async 返回 Promise Chapter 5: Async/Await text/08-ch05-chapter-5-async-await.txt:15(搜「fetchData」) · text/08-ch05-chapter-5-async-await.txt:21(搜「always return」)
"看起来同步,实际仍异步" 同上 text/08-ch05-chapter-5-async-await.txt:42(搜「doesn't actually make」)
重构对照 同上 text/08-ch05-chapter-5-async-await.txt:50(搜「fetchUserData」) · text/08-ch05-chapter-5-async-await.txt:108(搜「declared as async」)
try/catch 与 response.ok 同上 text/08-ch05-chapter-5-async-await.txt:126(搜「fetchData」) · text/08-ch05-chapter-5-async-await.txt:130(搜「response.ok」)
顶层兜底 同上 text/08-ch05-chapter-5-async-await.txt:177(搜「unhandledRejection」)
Promise.all 组合 同上 text/08-ch05-chapter-5-async-await.txt:222(搜「Promise.all」)
allSettled 变体 同上 text/08-ch05-chapter-5-async-await.txt:276(搜「allSettled」)
async 没有取代 Promise 同上 text/08-ch05-chapter-5-async-await.txt:118(搜「doesn't replace Promises」)

本章的机械翻译规则与三层兜底模板,可以直接当团队代码评审的检查单用;评审时重点盯三处:并行段还在不在、catch 的位置对不对、有没有裸奔的 await(没有 try/catch 的 async 函数)。

下一章把这些机制装配成真正的工程模式:重试、退避、限流。


  1. 出处:「Chapter 5: Async/Await」第 9-13 段(text/08-ch05-chapter-5-async-await.txt:11,搜「syntactic sugar」)。原文称 async/await 为"built on top of Promises"的 syntactic sugar(语法糖)。 ↩

  2. 出处:同章第 42 段(text/08-ch05-chapter-5-async-await.txt:42,搜「doesn't actually make」)。原文:"while async/await makes asynchronous code look synchronous, it doesn't actually make the code run synchronously." ↩

  3. 出处:同章第 15-23 段(text/08-ch05-chapter-5-async-await.txt:21,搜「always return」;:23,搜「only be used」)。async 函数"will always return a Promise";await"allows you to write asynchronous code that looks synchronous"。 ↩↩

  4. 出处:同章第 46-104 段(text/08-ch05-chapter-5-async-await.txt:50,搜「fetchUserData」)。重构前后的两段代码照原书。 ↩

  5. 出处:同章第 106-118 段(text/08-ch05-chapter-5-async-await.txt:116,搜「straightforward」)。原文:"if we needed to add another API call or perform some data processing… it would be straightforward to insert the new code"。 ↩↩

  6. 出处:同章第 122-173 段(text/08-ch05-chapter-5-async-await.txt:159,搜「response.ok」;:139,搜「re-throw」)。response.ok 检查与"Re-throw the error if you want calling code to handle it"照原书。 ↩↩

  7. 出处:同章第 175-195 段(text/08-ch05-chapter-5-async-await.txt:177,搜「unhandledRejection」;:193,搜「slip through」)。全局处理+顶层 try/catch 的双重兜底照原书。 ↩

  8. 出处:同章第 199-268 段(text/08-ch05-chapter-5-async-await.txt:222,搜「Promise.all」)。getUserInfo 三请求并行的组合写法照原书。 ↩

  9. 出处:同章第 270-296 段(text/08-ch05-chapter-5-async-await.txt:276,搜「allSettled」)。allSettled 版本逐张判决、失败填 null 的处理照原书。 ↩

  10. 出处:同章第 118 段(text/08-ch05-chapter-5-async-await.txt:118,搜「doesn't replace Promises」)。原文:"while async/await provides a more intuitive way to work with Promises, it doesn't replace Promises entirely." ↩

  11. 出处:「Chapter 5: Async/Await」第 199-296 段(text/08-ch05-chapter-5-async-await.txt:222,搜「Promise.all」;:276,搜「allSettled」)。并行组合在 async 世界的保留与 allSettled 变体照原书。 ↩

  12. 出处:「Chapter 6: Common Patterns and Use Cases」第 63-66 段(text/09-ch06-chapter-6-common-patterns-and-use-cases.txt:63,搜「async ()」)。立即执行 async 箭头函数的用法照原书;四种函数形态为对 async 关键字语法的一般说明(补充,不在书里)。 ↩

  13. 出处:「Chapter 5: Async/Await」第 40-42 段(text/08-ch05-chapter-5-async-await.txt:42,搜「doesn't actually make」)。"doesn't actually make the code run synchronously… allowing other code to execute" 照原书。 ↩

  14. 出处:「Chapter 5: Async/Await」第 199-296 段(text/08-ch05-chapter-5-async-await.txt:222,搜「Promise.all」;:276,搜「allSettled」)。两版本的行为与消费代码照原书。 ↩

  15. 出处:「Chapter 5: Async/Await」第 175-195 段(text/08-ch05-chapter-5-async-await.txt:177,搜「unhandledRejection」;:182,搜「main」)。全局处理+顶层 try/catch+main() 的结构照原书。 ↩