Promise异步编程中那些你必须掌握的核心原理与实战技巧

珮青的笔记 前端 阅读 1,924
赞 36 收藏
二维码
手机扫码查看
反馈

谁更灵活?谁更省事?Promise、async/await、还有那个“被遗忘的”then链

我写这篇,是因为上周又在 Code Review 里看到同事用 Promise.resolve().then(() => {...}).then(() => {...}) 套了五层,还加了 catch 在最外层——结果一个 throw new Error('xxx') 没被 catch 住,因为中间某层写了 return Promise.reject(...) 却没接 catch。线上报错,查了半小时才发现是 Promise 链断了。

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 分钟,我能多喝一杯咖啡,或者早点下班陪娃写作业。

以上是我踩坑后的总结,希望对你有帮助。有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论