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 的核心价值就两个:
- 状态管理:pending → fulfilled / rejected,状态一旦改变就不可逆
- 链式调用 :用
.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 接住
六、总结:记住这三句话就够了
- Promise 链里一定要 return,否则下一个 then 收到 undefined
- 错误处理要覆盖全链路,catch 只管前面的
- 能用 async/await 就用,复杂场景配合 Promise.all/allSettled