Promise 从入门到踩坑:为什么你的异步代码还是一团乱麻

Promise 从入门到踩坑:为什么你的异步代码还是一团乱麻

你以为用了 Promise 就万事大吉?天真了,少年。


一、先问个扎心的问题

你写过这样的代码吗?

ini 复制代码
fetch('/api/user')
  .then(res => res.json())
  .then(user => {
    fetch(`/api/orders?userId=${user.id}`)
      .then(res => res.json())
      .then(orders => {
        fetch(`/api/details?orderId=${orders[0].id}`)
          .then(res => res.json())
          .then(detail => {
            console.log('终于拿到了', detail)
          })
      })
  })

恭喜你,你已经成功把回调地狱升级成了 Promise 地狱------换汤不换药,一样辣眼睛。


二、Promise 到底解决了什么问题?

先复习一下最基础的东西。Promise 的核心价值就两个:

  1. 状态管理:pending → fulfilled / rejected,状态一旦改变就不可逆
  2. 链式调用 :用 .then() 代替嵌套回调,让代码扁平化

但很多人只学到了第二点的一半------知道要链式调用,却不知道怎么正确地链

上面那段代码的正确写法应该是:

ini 复制代码
fetch('/api/user')
  .then(res => res.json())
  .then(user => fetch(`/api/orders?userId=${user.id}`))
  .then(res => res.json())
  .then(orders => fetch(`/api/details?orderId=${orders[0].id}`))
  .then(res => res.json())
  .then(detail => console.log('终于拿到了', detail))
  .catch(err => console.error('翻车了', err))

关键区别是什么?每个 .then() 都 return 一个新的 Promise ,而不是在里面再套一层 .then()

如果你记不住别的,请记住这句话:

在 Promise 链里,return 就是王道。


三、五个让你原地裂开的踩坑现场

坑位 1:忘记 return,然后疯狂 debug

scss 复制代码
getUser()
  .then(user => {
    getOrders(user.id)  // ❌ 没有 return!
  })
  .then(orders => {
    console.log(orders)  // undefined,崩溃.jpg
  })

原因 :上一个 .then() 没有返回值,下一个 .then() 收到的就是 undefined

解法 :养成肌肉记忆------只要想继续传值,就写 return

scss 复制代码
getUser()
  .then(user => getOrders(user.id))  // ✅ 隐式 return
  .then(orders => console.log(orders))

坑位 2:以为 catch 能救一切,结果它只救前面

javascript 复制代码
doSomething()
  .then(() => {
    throw new Error('炸了')
  })
  .catch(err => {
    console.log('补救一下')
    return '修复值'
  })
  .then(result => {
    console.log(result)  // '修复值' ✅
  })
  .then(() => {
    throw new Error('又炸了')  // ❌ 这个 catch 不到!
  })

真相.catch() 只管它前面的错误,后面的错误需要另一个 .catch()

解法:要么在整个链末尾加一个全局 catch,要么在每个可能出错的地方单独处理。

坑位 3:try/catch + async/await 混用,漏掉 silent error

javascript 复制代码
async function main() {
  try {
    const user = await getUser()
    const orders = await getOrders(user.id)  // 假设这里 reject 了
  } catch (err) {
    console.log('捕获到了', err)
  }
}

看起来没问题对吧?但如果 getOrders 里面的错误是在微任务里抛出的呢?

javascript 复制代码
async function getOrders(id) {
  // 这玩意儿没被 await,错误会变成 unhandled rejection
  somePromise.then(data => { throw new Error('hidden error') })
  return await fetch(...)
}

这种错误 try/catch 抓不到,因为它发生在另一个 Promise 链上。

解法:确保所有 Promise 都被 await 或者有 catch 兜底,不要有"野生的 Promise"。

坑位 4:Promise.all 一颗老鼠屎坏了一锅粥

javascript 复制代码
const promises = [task1, task2, task3]
const results = await Promise.all(promises)
// 任何一个 reject,全部玩完

有时候你想要的是"谁失败了我不管,成功的我要拿到",这时候应该用 Promise.allSettled

javascript 复制代码
const results = await Promise.allSettled(promises)
results.forEach(r => {
  if (r.status === 'fulfilled') console.log(r.value)
  else console.warn('失败了但不影响大局', r.reason)
})

还有一个冷知识:Promise.all快速失败的------只要有一个 reject,立刻 reject,其他还在 pending 的结果你就永远拿不到了。如果你的任务有副作用(比如发邮件、写数据库),这就很危险。

坑位 5:Promise.race 的"幽灵回调"

javascript 复制代码
const controller = new AbortController()

const timeout = new Promise((_, reject) => {
  setTimeout(() => reject(new Error('超时')), 5000)
})

Promise.race([fetch(url, { signal: controller.signal }), timeout])
  .then(response => {
    controller.abort()  // 取消请求
    return response.json()
  })
  .catch(err => console.log('超时或失败', err))

看起来很完美?问题是:即使 race 已经结束了,timeout 的定时器还在跑,它只是 Promise 的状态不会再改变而已。

更隐蔽的是,如果 fetch 赢了,那个 timeout 的 reject 永远不会发生,但它内部的 setTimeout 回调仍然会在 5 秒后执行------虽然这次不会触发任何东西,但如果里面有闭包引用,内存泄漏就来了。

解法 :用 AbortController 真正取消不需要的操作,或者在 Promise 构造函数里加清理逻辑。


四、进阶:你真的理解 then 的回调函数吗?

来,看一道面试题:

javascript 复制代码
Promise.resolve('hello')
  .then(console.log)           // 输出 hello
  .then(Promise.resolve('world'))  // 猜猜这个输出什么?
  .then(console.log)           // 输出什么?

答案是:第一个 console.log 输出 hello,第二个输出 undefined

为什么? ​ 因为 Promise.resolve('world') 是一个立即执行的表达式,它返回的是一个 Promise 对象,不是一个函数

.then() 期望的参数是函数。当你传了一个非函数进去(比如一个 Promise 实例),它会自动忽略,直接把上一个值传递下去。

所以 then(Promise.resolve('world')) 等价于 then(value => value),啥也没干,下一个 then 拿到的还是上一个 then 的返回值------也就是 console.log 的返回值 undefined

正确写法应该是:

javascript 复制代码
.then(() => Promise.resolve('world'))

五、终极建议:别折腾,用 async/await

说了这么多,其实最终的建议很简单:

能用 async/await 就别手写 then/catch。

javascript 复制代码
// ✅ 清晰得像同步代码
async function getOrderDetail() {
  try {
    const user = await getUser()
    const orders = await getOrders(user.id)
    const detail = await getDetail(orders[0].id)
    return detail
  } catch (err) {
    console.error('全程统一处理错误', err)
    throw err
  }
}

但注意,async/await 不是银弹:

  • 多个无依赖的异步操作,记得用 Promise.all 并行,不要一个个 await
  • forEach 里的 await 不会等,要用 for...of
  • async 函数返回的一定是个 Promise,外面要用 await 或者 then 接住

六、总结:记住这三句话就够了

  1. Promise 链里一定要 return,否则下一个 then 收到 undefined
  2. 错误处理要覆盖全链路,catch 只管前面的
  3. 能用 async/await 就用,复杂场景配合 Promise.all/allSettled
相关推荐
颜进强1 小时前
Calude Code - 25 CodeGraph:让 AI 真正读懂你的代码库
前端·后端·ai编程
七牛开发者1 小时前
Agent 小知识|长任务不重来:Agent 状态保存的工程设计
前端·javascript·后端
二月龙1 小时前
JS 事件循环完整解析:宏任务、微任务,浏览器到底怎么执行代码
后端
长大19881 小时前
写了多年 JS 却仍被“变量提升”拿捏?这篇文章一次讲透
后端
大勇前进1 小时前
闭包到底有什么用?别只背概念,看完秒懂实际业务场景
后端
神奇小汤圆1 小时前
深入理解 TiDB 分布式事务:Percolator 模型与工程实践
后端
神奇小汤圆2 小时前
Flink SQL 从编写到提交运行的全过程解析
后端
饼干哥哥2 小时前
Codex 必改的8 个基础配置
前端·人工智能·后端
lingran__2 小时前
Linux 基础常用指令万字详解(上)|文件目录命令,结合底层原理剖析
linux·运维·服务器·后端·centos·linux基础指令