前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
首页程序员面试题库async/await vs Promise 链
JavaScript异步与 Promise

为什么说 async/await 是 Promise 的语法糖?能把它改写成 then 链吗?

async 函数返回的就是 Promise,await 这一行之后的代码,等价于写进前一个 Promise 的 then 回调里。try/catch 对应 .catch(),而 finally 对应 .finally()。语法换了,执行模型还是 Promise 加微任务。

前端进阶之旅 · 一题精讲更新于 2026.09.29
JavaScript#异步与 Promise#异步编程
先看核心答案读代码示例
理解线索

await 写法和 then 链的对应关系

  1. await p等价于 p.then(...),后面的代码都进了回调
  2. try/catch等价于在链上接 .catch(),接住前面任意一步的拒绝
  3. async 函数等价于一个返回 Promise 的普通函数

await 暂停的只是当前 async 函数,调用方拿到 Promise 后照常往下执行,主线程从来没有被阻塞。

核心回答

先记住这个答案

说它是语法糖,是因为 async/await 没有引入新的异步机制,底下跑的还是 Promise。async 函数一定返回 Promise,await p 会把函数剩下的部分挂到 p 完成之后执行,效果和写进 p.then() 的回调一样。错误处理也一一对应:try/catch 相当于 .catch(),而 finally 相当于 .finally()。它解决的是可读性问题,让异步代码能按从上到下的顺序写,而不是一层层回调。

  • async 函数返回 Promise,return 值就是兑现值
  • await 之后的代码相当于 then 回调,在微任务里执行
  • try/catch 对应 .catch(),而 finally 对应 .finally()
  • 转译到 ES2015 时,靠 Generator 加自动执行器实现

同一段逻辑的两种写法

业务里最常见的是「先拿用户,再用用户 id 拿订单」这种有依赖的两步请求。分别用 await 和 then 链写一遍,对照着看每一行去了哪里。

await 版和 then 链版JavaScript
const getUser = () => Promise.resolve({ id: 7 })
const getOrders = (id) => Promise.resolve(['订单A', '订单B'].map((o) => o + id))

async function withAwait() {
  const user = await getUser()
  const orders = await getOrders(user.id) // 这一行相当于第一个 then 回调
  return orders.join(',') // 相当于第二个 then 回调的返回值
}

function withThen() {
  return getUser()
    .then((user) => getOrders(user.id))
    .then((orders) => orders.join(','))
}

withAwait()
  .then((r) => console.log('await 版', r))
  .then(() => withThen())
  .then((r) => console.log('then 版', r))
查看输出与解释
await 版 订单A7,订单B7
then 版 订单A7,订单B7

两版结果完全一样。对应规则就是:每个 await 都把后面的代码切成一段,放进一个 then 回调里;await 左边的变量,就是回调的参数;函数最后 return 的值,就是整条链最终兑现的值。

await 之后的代码在什么时候执行

「放进 then 回调」这句话有一个直接的推论:await 之后的代码不会同步执行,而是等到微任务阶段。微任务是当前同步代码跑完后、浏览器渲染和下一个定时器之前,统一执行的一批回调。

await 把函数切成两半JavaScript
async function task() {
  console.log('1 await 之前,同步执行')
  await null // 哪怕等的不是 Promise,也会让出执行权
  console.log('3 await 之后,在微任务里执行')
}

task()
console.log('2 调用方继续往下走')
查看输出与解释
1 await 之前,同步执行
2 调用方继续往下走
3 await 之后,在微任务里执行

await 之前的部分和普通函数一样,调用时立刻同步执行。遇到 await 时函数暂停,把控制权交还给调用方,所以 2 先打印。改写成 then 链就是 console.log(1); return Promise.resolve(null).then(() => console.log(3)),顺序一模一样。

错误处理的对应关系
try { await a(); await b() } catch (e) { handle(e) } finally { done() } 对应的是 a().then(() => b()).catch(handle).finally(done)。catch 能接住 a 或 b 任意一步的拒绝,链上的 .catch() 也一样,前面任何一环失败都会跳过中间的 then 直接落到它。

两种写法不完全等价的地方

语义上能互相改写,但写起来的难度差很多。then 链里每个回调是一个独立的作用域,第三步想同时用到第一步的 user 和第二步的 orders,要么嵌套回调,要么把两个值打包往下传。await 版本里它们都是同一个函数的局部变量,直接用就行。

循环和条件分支
「逐个请求直到拿到空页」这种循环,用 await 写就是一个普通的 while。用 then 链写,得写成递归函数,每次在 then 里决定要不要再调用自己。条件分支也一样,await 版本就是 if/else,换成 then 链要在回调里返回不同的 Promise。这正是 async/await 被发明出来的原因。 至于底层实现,现代引擎是原生支持 async 函数的。把代码转译到 ES2015 时,Babel 和 TypeScript 会把它变成 Generator 加一个自动执行器:Generator 每 yield 一个 Promise,执行器就在它的 then 里调用 next() 把结果送回去,这也从侧面说明了它和 then 链的等价关系。
回答前,多想一步

容易答错的地方

以为 await 会阻塞线程
await 只暂停它所在的 async 函数,线程马上去执行调用方的其他代码和别的任务。上面示例里 2 先于 3 打印就是证据。真正会卡住页面的是长时间运行的同步代码,和有没有写 await 无关。
以为写成 await 之后就不需要处理拒绝了
await 一个 rejected Promise 会在这一行抛出异常。不写 try/catch,这个异常会让 async 函数返回的 Promise 变成 rejected,调用方再不处理,就成了未处理的拒绝。改成 await 写法后,原来链尾的 .catch() 要换成 try/catch,不能直接删掉。
以为两种写法的微任务时序完全相同
结果和先后依赖是等价的,但两条独立的链交错执行时,await 版和 then 版各自需要的微任务轮数不一定一样。业务代码不要依赖两条不相关的异步链谁先谁后,有顺序要求就显式地串起来。
试着用自己的话回答

面试官还会怎么问?

能用 Generator 手写一个简易的 async 执行器吗?

可以。写一个 run(genFn):先调用 genFn() 拿到迭代器,定义 step(value),内部调用 it.next(value)。如果 done 为 true 就用结果 resolve;否则把 yield 出来的值用 Promise.resolve 包一下,在 then 里继续调用 step,在拒绝回调里调用 it.throw(err)。整个函数返回一个 Promise,这就是 co 库的核心思路。

await 后面跟的不是 Promise 会怎样?

会先用 Promise.resolve 包一下再等。所以 await 1 拿到的就是 1,但依然会让出一次执行权,后面的代码要等到微任务阶段才执行。如果后面是带 then 方法的对象,会调用它的 then 来取值。

并发请求用 await 怎么写?

不能连着写两个 await,那是串行。要先同时发起,再用 await Promise.all([a(), b()]) 统一等待。Promise.all 本身就是 Promise 的 API,这也说明 async/await 离不开 Promise,只是换了一种写法。

从一道题,走向一组知识

把知识连起来

异步与 Promise

async/await 里哪些错误该用 try/catch 接,哪些不该接?

知道 try/catch 对应 .catch() 之后,下一步就是弄清一个 try 到底能接住哪些 await 的错误。

异步与 Promise

并发池里任务谁先完成不一定,怎么让结果还按原来的顺序排?

并发池里每个任务都是一条独立的 then 链,理解了 await 和 then 的对应关系,才能看懂结果为什么按完成顺序回来。

参考资料

  • JavaScript:异步与 Promise参考资料

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 同一段逻辑的两种写法
  3. await 之后的代码在什么时候执行
  4. 两种写法不完全等价的地方
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑