先记住这个答案
then 调用后本来就会返回一个新的 Promise,成功值和错误都会自动沿链传下去。如果再用 new Promise 把它包起来,状态就改成了靠你手动调用 resolve 和 reject 来决定,漏掉任何一个分支,外层 Promise 就永远停在 pending,调用方既拿不到结果也收不到错误。改法很简单:直接 return 原有的 Promise 链,或者改成 async 函数。new Promise 只在把 setTimeout、回调风格的老 API 转成 Promise 时才需要。
then已经返回新 Promise,不需要再包- 手动包裹时漏写
reject,外层会永远 pending - 改法:直接
return原链,或改成async函数 new Promise只留给回调式 API 转 Promise
先看错误是怎么丢的
下面两个函数做同一件事:拿到用户信息后取出名字。区别只在有没有多包一层。
// 模拟一个本来就返回 Promise 的请求函数
const getUser = () => Promise.resolve({ name: 'Tom' })
// 反模式:多包一层 new Promise,只写了 resolve
function loadNameBad() {
return new Promise((resolve) => {
getUser().then((user) => resolve(user.profile.name)) // user.profile 是 undefined,这里抛 TypeError
})
}
// 正确写法:直接返回原有的链
function loadNameGood() {
return getUser().then((user) => user.profile.name)
}
loadNameBad().then(console.log, (e) => console.log('bad 收到:', e.name)) // 什么都不会打印
loadNameGood().then(console.log, (e) => console.log('good 收到:', e.name)) // good 收到: TypeErrorloadNameBad 的问题出在内层 then 的回调里。user.profile.name 抛出的 TypeError 只会让内层那个 then 返回的 Promise 变成 rejected,而外层 new Promise 的状态只认 resolve 和 reject 两个函数,谁都没被调用,它就一直 pending。调用方挂的错误处理永远等不到消息,浏览器控制台只会多一条 Unhandled Rejection。
loadNameGood 什么都没多写,错误自然就传到了调用方。
为什么 executor 救不了你
有人会说:new Promise 的 executor 里抛错不是会自动变成 reject 吗?这话只对了一半。executor 是同步执行的,它只能捕获执行那一刻同步抛出的异常。
- 内层回调已经不在 executor 里了
- 上面例子里,executor 同步执行完只做了一件事:调用
getUser()并挂上then。等内层回调真正执行、真正抛错时,已经是之后的微任务了,executor 早就返回了。这就像你把快递交给驿站就走了,包裹后来丢了,驿站没通知你,你永远不知道。 所以用包裹写法时,必须在内层补上.catch(reject),或者写成getUser().then(resolve, reject)这种形式,每一处都不能漏。代码一复杂,分支一多,漏掉的概率就很高。而原生链式写法根本不存在这个问题。
正确的改法和唯一该用的场景
改造的原则是:能 return 就不要 resolve。已有 Promise 的地方直接返回它,需要加工结果就接一个 then,需要兜底就接一个 catch。
- 改成
async函数更直观 - 写成
async function loadName() { const user = await getUser(); return user.profile.name },返回值自动包成 Promise,内部抛出的TypeError自动变成拒绝,和链式写法效果一样,读起来还像同步代码。 - 什么时候确实需要
new Promise - 只有底层 API 不返回 Promise 的时候。比如把
setTimeout包成一个等待函数,写法是const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))。再比如把 Node.js 风格的(err, data) => {}回调转成 Promise,这时候new Promise是在给没有 Promise 的东西造一个 Promise,不是重复包装。Node.js 里这类转换也可以直接用util.promisify。
容易答错的地方
- 以为只是代码啰嗦,功能上没区别
- 啰嗦是小事,真正的问题是错误处理。原链的失败会自动传递,包裹之后失败要靠手动转发,漏一个分支就是一个永远不结束的 Promise,页面上表现为 loading 一直转,而且没有报错可查。
- 以为在 executor 外面套
try/catch就安全了 try/catch同样只管同步代码。内层then回调里的异常发生在后面的微任务,外面的try/catch早已执行完毕,一样捕获不到。
面试官还会怎么问?
包裹写法里补上 .catch(reject) 是不是就没问题了?
功能上能正常工作,但还是多余。你写的 resolve 和 reject 转发,就是 then 本来已经在做的事。除非你要在转发前做额外的事,否则直接返回原链更短也更不容易出错。
给一个已有的请求加超时,需要 new Promise 吗?
请求本身不用包。只需要单独造一个会在指定时间后拒绝的 Promise,这一步要用 new Promise 包 setTimeout,然后用 Promise.race([request, timeout]) 组合。如果请求支持 signal,直接传 AbortSignal.timeout(ms) 更好,超时后请求会被真正取消。
return 一个 Promise 和 resolve 一个 Promise 有什么区别?
效果基本一样,外层都会跟随内层 Promise 的最终状态,内层失败外层也失败。区别在于 return 发生在 then 或 async 函数里,失败会自动传递;而在手写的 executor 里,只有你显式调用了 resolve(innerPromise) 才会跟随,前提还是得记得调。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。