Appearance
JavaScript 中的 Promise、async/await 与异步编程
JavaScript 的异步编程,是很多前端工程问题的核心主线之一。
如果只记“Promise 比回调高级”“async/await 更好用”,其实还不够。更贴近这条主线的问题是:
异步任务的结果如何被组织、传递、等待和错误处理。
1. 为什么 JavaScript 要强调异步
因为浏览器里的很多任务都不是“立刻就能得到结果”。
例如:
- 网络请求
- 定时器
- 用户交互事件
- 文件读取
如果主线程必须一直停在那里等结果,页面体验就会非常差。
所以 JavaScript 需要一套方式,让代码可以:
- 先发起任务
- 以后再拿结果
- 在结果回来后继续后续逻辑
2. 回调为什么会先遇到问题
最早最直观的方式是回调函数。
js
setTimeout(() => {
console.log('done')
}, 1000)回调本身不是错的,它的问题主要出在:
- 多层异步嵌套时可读性变差
- 错误处理不统一
- 结果传递链条容易变乱
3. Promise 到底是什么
Promise 可以看成 一个代表“异步操作未来结果”的对象。
它解决的是 异步结果不要靠层层回调拼起来,而是通过状态变化和链式调用组织起来。
4. Promise 的三种状态
Promise 主要有三种状态:
pendingfulfilledrejected
把这三种状态看成:
pending:还没完成fulfilled:成功完成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. then、catch、finally 在干什么
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把这层机制拆开看:
then不会直接把结果“改写”回原 Promise- 它会创建并返回一个新的 Promise
- 这个新 Promise 的状态,取决于回调函数执行后的结果
这就是为什么:
- 回调里返回普通值,后面的
then会收到这个值 - 回调里返回另一个 Promise,后面的链会继续等待它
- 回调里抛错,后面的链会进入失败分支
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 本身,而在于:
- 第一个
then没有把requestOrders(...)返回出去 - 所以后面的链拿不到那个 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 接住。
实际写项目时,更多是这样:
- Promise 链会把回调中的异常转成拒绝态
- 后面的
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
}
}这里最值得记住的是:
async函数默认返回 Promiseawait后面通常跟一个 Promiseawait会让当前函数后续代码等待结果,但不会让整个 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 把函数切成了两段“神秘代码”,而是:
await前面的部分先同步执行await后面的继续逻辑会被放到后续微任务里恢复
所以更贴近执行机制的说法是:await 不是让代码消失了,而是让后半段在 Promise 完成后以微任务形式继续运行。`
8. Promise 和 async/await 是什么关系
可以直接把它理解成:
- Promise 是异步结果模型
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. 为什么错误处理常常写乱
异步错误最容易乱在这里:
- 有些错误来自 Promise
reject - 有些错误来自同步代码
throw - 有些错误发生在链条中间某一段
工程上比较稳的做法通常是:
- Promise 链统一在合适位置用
catch async/await场景下优先用try...catch- 不要让错误既局部吞掉、又在外层重复兜底到看不清边界
10.1 为什么“吞错”会让异步链更难排查
看下面这个例子:
js
requestUser()
.then(user => {
try {
return JSON.parse(user.profile)
} catch (error) {
console.error(error)
return null
}
})
.then(profile => {
console.log(profile)
})这类写法的问题不是不能跑,而是:
- 这里把错误吃掉了
- 后面只能拿到一个模糊的
null - 业务链路会越来越难分清到底是“正常空值”还是“中间失败”
所以工程上更稳的方式通常是:
- 真正能恢复时再局部兜底
- 如果当前层无法恢复,就继续把错误抛出去
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 本身并不会等待异步回调,所以这段代码更接近:
- 快速把每个异步任务都发出去了
- 外层流程并没有等待这些任务真正结束
如果你要真正串行,通常更适合:
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 创建了,任务就一定已经被正确等待
真正被等待的前提,是:
- 你把 Promise 返回了
- 或者你显式
await了 - 或者你把它纳入
Promise.all一类控制流里了
否则工程里很容易出现:
- 请求其实已经发了
- 但外层流程提前结束
- 错误也没有被统一接住
14. 小结
这条主线可以压缩成几句话:
- Promise 是 JavaScript 组织异步结果的核心模型
async/await是建立在 Promise 之上的更自然写法- 异步编程真正要解决的是结果传递、错误处理和执行顺序
- 选择串行还是并发,取决于任务之间是否互相依赖
下一步如果继续往下看,最自然的是进入 模块化、ES Module 与运行时边界,因为真实工程里的异步逻辑通常不会孤立存在,而是嵌在模块体系里。