Skip to content

JavaScript 中的 Promise、async/await 与异步编程

JavaScript 的异步编程,是很多前端工程问题的核心主线之一。

如果只记“Promise 比回调高级”“async/await 更好用”,其实还不够。更贴近这条主线的问题是:

异步任务的结果如何被组织、传递、等待和错误处理。

1. 为什么 JavaScript 要强调异步

因为浏览器里的很多任务都不是“立刻就能得到结果”。

例如:

  1. 网络请求
  2. 定时器
  3. 用户交互事件
  4. 文件读取

如果主线程必须一直停在那里等结果,页面体验就会非常差。

所以 JavaScript 需要一套方式,让代码可以:

  1. 先发起任务
  2. 以后再拿结果
  3. 在结果回来后继续后续逻辑

2. 回调为什么会先遇到问题

最早最直观的方式是回调函数。

js
setTimeout(() => {
  console.log('done')
}, 1000)

回调本身不是错的,它的问题主要出在:

  1. 多层异步嵌套时可读性变差
  2. 错误处理不统一
  3. 结果传递链条容易变乱

3. Promise 到底是什么

Promise 可以看成 一个代表“异步操作未来结果”的对象

它解决的是 异步结果不要靠层层回调拼起来,而是通过状态变化和链式调用组织起来

4. Promise 的三种状态

Promise 主要有三种状态:

  1. pending
  2. fulfilled
  3. rejected

把这三种状态看成:

  1. pending:还没完成
  2. fulfilled:成功完成
  3. rejected:执行失败

要特别注意:Promise 一旦从 pending 进入 fulfilled 或 rejected,通常就不会再反复切换。

4.1 Promise 构造函数里的执行器为什么会“立刻执行”

这是 Promise 里一个非常高频的误区。

很多人会以为只要写了 new Promise(...),里面的逻辑就天然变成异步了。

其实不是。

js
console.log('start')

const p = new Promise((resolve) => {
  console.log('executor')
  resolve('done')
})

console.log('end')

输出顺序通常是:

js
start
executor
end

这说明一件事:Promise 构造函数里的执行器函数本身是同步立即执行的,异步的是状态变更后的回调调度。

这个区分非常重要,因为它能解释很多“为什么这段代码没有延后”的问题。

5. thencatchfinally 在干什么

5.1 then

用来拿成功结果。

5.2 catch

用来统一处理错误。

5.3 finally

用来做“无论成功还是失败都要执行”的收尾逻辑。

js
fetch('/api/user')
  .then(res => res.json())
  .then(data => {
    console.log(data)
  })
  .catch(err => {
    console.error(err)
  })
  .finally(() => {
    console.log('finish')
  })

5.4 then 为什么总是返回一个新的 Promise

这是 Promise 链能成立的核心原因之一。

js
const p1 = Promise.resolve(1)
const p2 = p1.then(value => value + 1)

console.log(p1 === p2) // false

把这层机制拆开看:

  1. then 不会直接把结果“改写”回原 Promise
  2. 它会创建并返回一个新的 Promise
  3. 这个新 Promise 的状态,取决于回调函数执行后的结果

这就是为什么:

  1. 回调里返回普通值,后面的 then 会收到这个值
  2. 回调里返回另一个 Promise,后面的链会继续等待它
  3. 回调里抛错,后面的链会进入失败分支

6. Promise 链为什么重要

Promise 真正的价值,不只是“看起来比回调高级”,而是:每一步都可以返回一个新值或新的 Promise,让后续逻辑继续按链条组织。

js
requestUser()
  .then(user => requestOrders(user.id))
  .then(orders => requestInvoice(orders[0].id))
  .then(invoice => {
    console.log(invoice)
  })
  .catch(err => {
    console.error(err)
  })

6.1 Promise 链里的值为什么能一路传下去

可以把它理解成:前一个 then 的返回值,会成为后一个 then 的输入。

js
Promise.resolve(1)
  .then(value => value + 1)
  .then(value => value * 10)
  .then(value => {
    console.log(value) // 20
  })

如果这条规则不清楚,很多链式写法就会显得像“黑盒魔法”。

6.2 为什么 Promise 链里忘记 return 会出问题

这是工程里很常见的一类 bug。

js
requestUser()
  .then(user => {
    requestOrders(user.id)
  })
  .then(orders => {
    console.log(orders) // 很可能是 undefined
  })

问题不在 Promise 本身,而在于:

  1. 第一个 then 没有把 requestOrders(...) 返回出去
  2. 所以后面的链拿不到那个 Promise 的结果

也就是说:Promise 链能不能把异步结果串起来,关键不只是写 then,而是要把下一步真正 return 出去。

6.3 错误为什么会沿着 Promise 链向后冒泡

Promise 链里一个很重要的稳定性来源,是错误传播模型相对统一。

js
Promise.resolve()
  .then(() => {
    throw new Error('boom')
  })
  .catch(err => {
    console.error(err.message)
  })

这里虽然是在 then 里同步 throw,但后面仍然可以被 catch 接住。

实际写项目时,更多是这样:

  1. Promise 链会把回调中的异常转成拒绝态
  2. 后面的 catch 会继续处理这个拒绝态

这也是为什么 Promise 比层层回调更容易统一错误边界。

7. async/await 的本质是什么

async/await 可以看成 基于 Promise 的语法层整理,让异步代码写起来更像同步流程

它并没有抛弃 Promise,而是建立在 Promise 之上。

js
async function loadUser() {
  try {
    const res = await fetch('/api/user')
    const data = await res.json()
    return data
  } catch (error) {
    console.error(error)
    throw error
  }
}

这里最值得记住的是:

  1. async 函数默认返回 Promise
  2. await 后面通常跟一个 Promise
  3. await 会让当前函数后续代码等待结果,但不会让整个 JavaScript 主线程停摆

7.1 await 后面的代码为什么总像“晚一点再执行”

这背后和微任务调度直接相关。

js
async function demo() {
  console.log('a')
  await Promise.resolve()
  console.log('b')
}

console.log('start')
demo()
console.log('end')

输出通常是:

js
start
a
end
b

原因不是 await 把函数切成了两段“神秘代码”,而是:

  1. await 前面的部分先同步执行
  2. await 后面的继续逻辑会被放到后续微任务里恢复

所以更贴近执行机制的说法是:await 不是让代码消失了,而是让后半段在 Promise 完成后以微任务形式继续运行。`

8. Promiseasync/await 是什么关系

可以直接把它理解成:

  1. Promise 是异步结果模型
  2. async/await 是基于 Promise 的更自然写法

也就是说:

async/await 更像写法层的整理,而 Promise 才是底层组织异步结果的核心模型。

9. 常见 Promise 工具方法怎么理解

9.1 Promise.all

适合 希望并发执行多个任务,并且全部成功后再继续

9.2 Promise.allSettled

适合 不要求全部成功,只想拿到每个任务最终结果

9.3 Promise.race

适合 谁先完成就先采用谁的结果

9.4 Promise.any

适合 只要有一个成功就继续,全部失败才报错

9.5 它们真正的差异不只是“API 名字不同”

如果从结果模型看,可以这样区分:

方法成功条件失败条件更像什么场景
Promise.all全部成功任意一个失败就整体失败强依赖并发任务
Promise.allSettled一定会拿到所有结果不会因为单个失败直接中断批量任务汇总
Promise.race谁先结束就采用谁谁先失败也会直接结束超时竞争、抢首个结果
Promise.any任意一个成功就成功全部失败才失败多源兜底

所以工程里不要只记名称,更要先问:我真正想要的是“全成才算成”,还是“只要先有结果就行”。

10. 为什么错误处理常常写乱

异步错误最容易乱在这里:

  1. 有些错误来自 Promise reject
  2. 有些错误来自同步代码 throw
  3. 有些错误发生在链条中间某一段

工程上比较稳的做法通常是:

  1. Promise 链统一在合适位置用 catch
  2. async/await 场景下优先用 try...catch
  3. 不要让错误既局部吞掉、又在外层重复兜底到看不清边界

10.1 为什么“吞错”会让异步链更难排查

看下面这个例子:

js
requestUser()
  .then(user => {
    try {
      return JSON.parse(user.profile)
    } catch (error) {
      console.error(error)
      return null
    }
  })
  .then(profile => {
    console.log(profile)
  })

这类写法的问题不是不能跑,而是:

  1. 这里把错误吃掉了
  2. 后面只能拿到一个模糊的 null
  3. 业务链路会越来越难分清到底是“正常空值”还是“中间失败”

所以工程上更稳的方式通常是:

  1. 真正能恢复时再局部兜底
  2. 如果当前层无法恢复,就继续把错误抛出去

11. 并发和串行怎么区分

11.1 串行

js
const a = await taskA()
const b = await taskB()
const c = await taskC()

这表示上一项完成后,下一项才开始。

11.2 并发

js
const [a, b, c] = await Promise.all([taskA(), taskB(), taskC()])

这表示多个任务可以一起发起,再统一等待结果。

所以 await 本身不等于“最优”,真正要看的是:这些任务之间到底有没有依赖关系。

11.3 为什么 forEach + await 经常不是你以为的串行

这是很常见的一类误解。

js
list.forEach(async item => {
  await save(item)
})

很多人会以为这会一个一个保存。

forEach 本身并不会等待异步回调,所以这段代码更接近:

  1. 快速把每个异步任务都发出去了
  2. 外层流程并没有等待这些任务真正结束

如果你要真正串行,通常更适合:

js
for (const item of list) {
  await save(item)
}

如果你要并发,再明确写成:

js
await Promise.all(list.map(item => save(item)))

这类差异非常值得讲清,因为它直接影响请求顺序、错误处理和整体耗时。

12. 一张异步主线图

mermaid
flowchart TD
    A[发起异步任务] --> B[返回 Promise]
    B --> C[等待状态变化]
    C --> D[成功进入 fulfilled]
    C --> E[失败进入 rejected]
    D --> F[then 或 await 继续处理]
    E --> G[catch 或 try catch 处理]

13. 工程上最容易踩的坑

13.1 误以为 await 会阻塞整个程序

它只是在当前异步函数里等待结果。

13.2 本来可以并发,却写成串行

这样会拖慢整体耗时。

13.3 Promise 链里忘记 return

会让后续拿不到预期结果。

13.4 只写成功逻辑,不写失败路径

这样线上问题很难排查。

13.5 误以为 Promise 创建了,任务就一定已经被正确等待

真正被等待的前提,是:

  1. 你把 Promise 返回了
  2. 或者你显式 await
  3. 或者你把它纳入 Promise.all 一类控制流里了

否则工程里很容易出现:

  1. 请求其实已经发了
  2. 但外层流程提前结束
  3. 错误也没有被统一接住

14. 小结

这条主线可以压缩成几句话:

  1. Promise 是 JavaScript 组织异步结果的核心模型
  2. async/await 是建立在 Promise 之上的更自然写法
  3. 异步编程真正要解决的是结果传递、错误处理和执行顺序
  4. 选择串行还是并发,取决于任务之间是否互相依赖

下一步如果继续往下看,最自然的是进入 模块化、ES Module 与运行时边界,因为真实工程里的异步逻辑通常不会孤立存在,而是嵌在模块体系里。

基于 VitePress 构建的个人技术笔记。