Promise异步编程中那些你必须掌握的核心原理与实战技巧
谁更灵活?谁更省事?Promise、async/await、还有那个“被遗忘的”then链
我写这篇,是因为上周又在 Code Review 里看到同事用 Promise.resolve().then(() => {...}).then(() => {...}) 套了五层,还加了 catch 在最外层——结果一个 throw new Error('xxx') 没被 catch 住,因为中间某层写了 return Promise.reject(...) 却没接 catch。线上报错,查了半小时才发现是 Promise 链断了。
说实话,Promise 异步写法看似简单,但真正在业务里混久了就会发现:不是“会不会用”的问题,而是“怎么用才不容易翻车”的问题。我这几年从手写 Promise.then 到全项目切 async/await,再到最近在写底层 SDK 时又悄悄把部分逻辑切回原生 Promise 链……踩的坑够写三篇博客了。今天不讲理论,只说我在真实项目里怎么选、为什么这么选、哪个方案让我少改三次代码、哪个让我半夜被 call 起来修 bug。
三种主流写法:我日常都在用的三个“姿势”
就目前(2024 年中),前端异步处理其实就三大主力:
- 纯 Promise 链式调用(.then/.catch/.finally)
- async/await + try/catch
- Promise.all + async/await 混合(也就是“并行+串行”组合技)
下面贴的是我最近在一个表单提交模块里写的三段等效逻辑:调用用户信息接口 → 校验权限 → 提交数据 → 展示结果。我们逐个看。
姿势一:Promise 链,看着清爽,实则暗流涌动
这是我刚入行最爱写的写法,因为“链式”听着就很酷。但现在回头看,它最大的问题是:错误边界模糊。你永远得想清楚:这个 reject 是该由当前 then 处理,还是该冒泡到后面?
fetch('https://jztheme.com/api/user')
.then(res => res.json())
.then(user => {
if (!user.hasPermission) {
throw new Error('权限不足');
}
return user;
})
.then(user => fetch('https://jztheme.com/api/submit', {
method: 'POST',
body: JSON.stringify({ userId: user.id })
}))
.then(res => res.json())
.then(data => console.log('提交成功', data))
.catch(err => {
// 注意:这里 catch 不一定能捕获所有错误!
// 比如上面 fetch 的 network error 会进这里,但中间某个同步 throw 可能也会进
console.error('出错了', err);
});
问题在哪?我踩过两次坑:
- 第一次:在第二个
.then里写了if (x) return Promise.reject('xxx'),结果这个 reject 没被下一个.then接,也没被catch捕获——因为Promise.reject()不等于throw,它只是返回一个 rejected promise,而链式里如果没显式.catch或.then(null, handler),就会静默失败; - 第二次:有人在某个
.then里忘了return,导致下一层拿到的是undefined,然后undefined.json()报错,但堆栈显示是 fetch 后面那行,实际根本不是 fetch 的锅。
结论:适合写工具函数、封装基础请求层(比如统一加 loading、错误 toast),但不适合复杂业务流程。我现在的做法是——只在 request utils 里用 Promise 链,业务层一律不用。
姿势二:async/await + try/catch,我的默认首选
我从去年起就把所有新业务模块默认切到了 async/await。不是因为它多高级,而是它让错误处理回归“人话”:try 里出错,就进 catch,没有例外,不看链、不猜冒泡、不数 .then 有几个。
async function handleSubmit() {
try {
const res = await fetch('https://jztheme.com/api/user');
const user = await res.json();
if (!user.hasPermission) {
throw new Error('权限不足');
}
const submitRes = await fetch('https://jztheme.com/api/submit', {
method: 'POST',
body: JSON.stringify({ userId: user.id })
});
const data = await submitRes.json();
console.log('提交成功', data);
} catch (err) {
console.error('出错了', err);
}
}
优点太实在了:
- 调试友好:断点打在哪一行,就停在哪一行,call stack 清晰;
- 逻辑线性:读代码像读伪代码,不用来回跳转看 .then 套了几层;
- 错误可控:所有同步 throw、await 后的 reject、甚至 await 一个 undefined 都会被同一个 catch 拦住(前提是没漏掉 await)。
唯一要注意的坑:别忘了 await。我上周刚修一个 bug:同事写了 const data = api.getData();(没 await),然后直接 data.result 访问,结果 data 是 Promise 对象,.result 是 undefined。这种低级错误 async/await 也救不了你——它只帮你处理异步错误,不帮你写对语法。
姿势三:Promise.all + async/await,高并发场景的“保命写法”
当你要同时拉 3 个无关接口(比如用户信息、配置项、未读消息数),我绝不用三个 await 串行。这时候我就切回 Promise.all ——但它不是独立方案,而是 async/await 的搭档。
async function loadDashboard() {
try {
const [user, config, notifications] = await Promise.all([
fetch('https://jztheme.com/api/user').then(r => r.json()),
fetch('https://jztheme.com/api/config').then(r => r.json()),
fetch('https://jztheme.com/api/notifications').then(r => r.json())
]);
renderDashboard({ user, config, notifications });
} catch (err) {
// 注意:Promise.all 默认“全失败才 reject”,但只要有一个失败,整个就 reject 了
// 如果你想“不管谁失败都继续”,得自己封装 Promise.allSettled
console.error('加载首页失败', err);
}
}
这里我其实更倾向用 Promise.allSettled,尤其做后台管理页时,用户信息加载失败,但配置和通知还能展示,体验更好。不过 allSettled 兼容性稍差(IE 全挂),所以现在我一般这样写:
async function loadDashboardRobust() {
const results = await Promise.allSettled([
fetch('https://jztheme.com/api/user').then(r => r.json()),
fetch('https://jztheme.com/api/config').then(r => r.json()),
fetch('https://jztheme.com/api/notifications').then(r => r.json())
]);
const [userRes, configRes, notifyRes] = results;
const user = userRes.status === 'fulfilled' ? userRes.value : null;
const config = configRes.status === 'fulfilled' ? configRes.value : {};
const notifications = notifyRes.status === 'fulfilled' ? notifyRes.value : [];
renderDashboard({ user, config, notifications });
}
这写法看起来啰嗦,但线上出问题时,至少不会整个页面白屏。这点小代价,我愿意付。
我的选型逻辑:不讲道理,只讲省事
总结一下我现在的规则:
- 单个接口、或有明确先后依赖的流程(比如登录 → 拉用户信息 → 初始化权限):无脑 async/await + try/catch;
- 多个无关接口并行加载:Promise.allSettled + async/await;
- 写 request 工具函数、拦截器、loading 状态管理:用 Promise 链封装,但只暴露一个返回 Promise 的方法,业务层不碰 .then;
- 需要精细控制 reject 流向(比如某些错误要重试,某些要上报,某些要忽略):那就老老实实上 Promise 链 + .catch 分支处理——但这种情况一年遇不到两次,真遇到了,我也宁愿多写几行,也不愿后期 debug 时对着一堆 then 发呆。
最后说句大实话:async/await 并不是技术上“最优”的方案,它只是让我每天少花 15 分钟纠结“这个错误到底在哪一层被吞了”。而节省下来的这 15 分钟,我能多喝一杯咖啡,或者早点下班陪娃写作业。
以上是我踩坑后的总结,希望对你有帮助。有更优的实现方式欢迎评论区交流。

暂无评论