人人都会AI编程

12.3 async /await 语法糖

更新时间:2026-07-11

Promise 解决了回调地狱,让异步代码可以链式串联。但 .then() 的写法仍然需要把后续逻辑放进回调函数里,当多个异步操作有依赖关系时,代码还是会越来越长。ES2017(ES8)引入的 async/await 彻底改变了这种局面——它让你用同步代码的写法来编写异步逻辑,读起来像白描,执行起来却丝毫不阻塞。

12.3.1 async 函数:返回值为 Promise 的工厂

在函数声明前加上 async 关键字,该函数就变成了异步函数。异步函数的调用结果永远是一个 Promise 对象:

async function fetchData() {
  return '数据';   // 相当于 return Promise.resolve('数据')
}

fetchData().then(data => console.log(data)); // 输出 "数据"

函数体内可以用 return 直接返回一个值,它会自动被 Promise.resolve() 包装。如果内部抛出异常,返回的 Promise 会变成 rejected 状态:

async function fail() {
  throw new Error('出错了');  // 相当于 return Promise.reject(new Error('出错了'))
}

fail().catch(err => console.log(err.message)); // 输出 "出错了"

这意味着 async 函数可以像普通函数一样处理返回值和外抛异常,调用方依然使用 .then/.catch 或继续使用 await 处理。

12.3.2 await 表达式:直取 Promise 结果

await 关键字只能出现在 async 函数内部。它后面通常跟一个 Promise 对象,整个 await 表达式 会暂停当前 async 函数的执行,等待该 Promise 变为 fulfilled 状态,然后返回 Promise 的成功值;如果 Promise 变为 rejected,await 会抛出这个错误(相当于对 reject 的值做了一个隐式抛出)。

async function getUser() {
  const response = await fetch('/api/user');  // 等待 fetch 完成
  const data = await response.json();         // 等待 json 解析
  return data;
}

response 回来之前,getUser 函数的执行会暂停,但主线程并不会被阻塞——事件循环依然可以处理其他任务。await 的“等待”是异步的、非阻塞的,只是 getUser 内部后续代码被推后执行。

await 后面也可以是普通值,它会立即被当成已解决的 Promise 返回:

async function example() {
  const a = await 42;   // 42
  return a + 1;
}

12.3.3 错误处理:try-catch 回归舞台

因为 async 函数的执行体看起来像同步代码,所以标准的 try...catch 语句可以直接捕获 await 抛出的 rejection:

async function queryData() {
  try {
    const data = await fetch('/api/data');
    return await data.json();
  } catch (err) {
    console.error('请求失败:', err);
    // 可以在此处返回兜底数据或者重新抛出
    return { default: true };
  }
}

这比 Promise 链末尾加 .catch() 更加自然,尤其当有多步异步操作时,可以精确定位是哪一步出错,不用像以往那样在长长的链上追索。

如果不使用 try-catch,也可以在调用 async 函数时用 .catch() 处理错误,但习惯上在 async 函数内部统一捕获能避免遗漏。

12.3.4 串行与并行:成也 await,败也 await

await 书写多个异步操作时,要特别注意顺序问题。

串行执行:一个接一个

async function sequential() {
  const a = await fetchA();  // 等待 A 完成
  const b = await fetchB();  // 再等待 B 完成
  return [a, b];
}

fetchAfetchB 之间没有依赖关系,上面的写法却让它们强制串行,总耗时 = A 耗时 + B 耗时,浪费了等待时间。

并行执行:一起出发

async function parallel() {
  const [a, b] = await Promise.all([fetchA(), fetchB()]);
  return [a, b];
}

先用 fetchA()fetchB() 分别启动两个异步任务(它们会同时发起请求),再将得到的两个 Promise 传给 Promise.all 等待一起完成。总耗时 ≈ max(A 耗时, B 耗时)。

这是 async/await 最常见的“坑”:能用并行解决的,不要写成串行。实践中可以用代码规范或 linter 规则(如 require-await)来避免无意识的性能损耗。

12.3.5 await 的限制与注意事项

  • 只能在 async 函数内部使用:在普通函数或全局作用域使用 await 会报语法错误。一些现代环境(如浏览器的模块顶层、Node.js 的 ES Module 顶层)开始支持 Top-level await,但仍需谨慎使用。
  • 不支持 await.then() 回调:在 Promise.then 回调中再使用 await 是毫无意义的,因为回调已经在 Promise 链中了。
  • 循环中的 await:在 for...of 或传统 for 循环中使用 await 是安全的,因为每次迭代都会等待。如果希望并发执行,应该把 Promise 收集进数组后用 Promise.all。注意 forEach 中的异步回调不会等待循环完成,因为它内部无法感知 async 函数返回的 Promise。

12.3.6 async/await 与 Promise 的关系

async/await 没有替代 Promise,而是建立在 Promise 之上的语法糖。任何 async 函数返回的都是 Promise,你可以继续用 .then() 链式处理;在 .then 回调里也可以 return 一个 Promise 给链。两者互补共生:

  • 现有 API(如 fetch)返回 Promise,天然适合 await
  • 需要并发、竞速等复杂控制时,直接使用 Promise.allPromise.race 等方法。
  • 封装一个返回 Promise 的工具函数,在 async 函数中 await 它即可,浑然一体。

12.3.7 最佳实践

  1. 不要滥用 await 串行:审视图中的多个异步操作是否相互独立,独立则用 Promise.all 并发。
  2. 总是在 async 函数中处理错误:用 try...catch 包裹可能出错的 await,或对函数返回的 Promise 统一添加 .catch()
  3. 避免 async 函数过于庞大:可以把多个 await 拆分成更小的 async 函数,方便组合与出错隔离。
  4. 拒绝 top-level await 滥用:只在模块顶层初始化真正需要前置异步操作的场景使用(如加载全局配置),不建议在业务组件顶层无节制使用,它会延迟模块的执行。
  5. 理解异步本质,保留“异步思维”:看起来像同步,但执行时机仍然是异步的。注意竞态问题、多次点击产生的重复请求等场景,需要配合防抖/节流、请求取消等机制。

掌握 async/await,你就能写出既好读又好维护的异步代码。它不是银弹,但配合 Promise、错误处理和并发控制,能让复杂的异步流程像同步代码一样条理清晰。