先记住这个答案
会。Promise 构造器在调用 executor 时,内部相当于包了一层 try...catch,executor 里同步抛出的错误会被捕获,然后当作拒绝原因调用 reject。所以 new Promise(() => { throw new Error('x') }) 得到的是一个 rejected 的 Promise,后面接 catch 能拿到这个 Error 对象。有两个例外要记住:如果 throw 之前已经 resolve 过,错误会被忽略;如果错误是在 executor 里的异步回调中抛出的,构造器捕获不到。
- executor 同步
throw等同于reject(err) resolve之后再throw,错误被静默忽略- 异步回调里的
throw构造器接不住,Promise 一直 pending - 外层
try...catch包new Promise抓不到这个错误
同步抛错和 reject 效果一样
先看最基本的情况。executor 里直接 throw,外面的代码不会中断,错误进了 catch。
throw 之后用 catch 接住JavaScriptconst p = new Promise(() => {
throw new Error('配置缺失') // 同步抛错
})
console.log('构造完成,代码没有中断')
p.catch((err) => {
console.log('捕获到:', err.message)
console.log('是 Error 实例:', err instanceof Error)
})查看输出与解释
构造完成,代码没有中断
捕获到: 配置缺失
是 Error 实例: true第一行打印说明错误没有从 new Promise 抛出去,同步代码照常往下执行。错误变成了 Promise 的拒绝原因,等微任务阶段交给 catch 回调。
这个行为很实用。比如 executor 里要先 JSON.parse 一段配置再发请求,JSON.parse 抛出的 SyntaxError 不用自己 try...catch,它会自动变成拒绝,调用方统一在 catch 里处理就行。
throw和reject的一个区别reject(err)只是改状态,后面的代码会继续执行;throw会立刻中断 executor,后面的语句不再运行。所以在 executor 里校验参数时,写throw或return reject(err)都可以,只写reject(err)不加return的话,后面的逻辑还会跑一遍。
两个接不住的边界
构造器能捕获,是因为 executor 执行时它就在旁边。离开这个前提,错误就管不了了。
resolve 之后再 throwJavaScriptconst p = new Promise((resolve) => {
resolve('已成功')
throw new Error('晚了一步') // 状态已锁定,这个错误被忽略
})
p.then(
(v) => console.log('fulfilled:', v),
(err) => console.log('rejected:', err.message)
)查看输出与解释
fulfilled: 已成功构造器确实捕获到了这个错误,也调用了 reject,但 Promise 状态只能改一次,已经是 fulfilled 的 Promise 不会再变。错误既不会进 catch,也不会在控制台报出来,是真正被吞掉了。线上排查时这种情况最难发现。
- 异步回调里
throw - 如果写成
new Promise(() => { setTimeout(() => { throw new Error('x') }, 0) }),定时器回调执行时 executor 早就返回了,构造器的那层try...catch已经不在调用栈上。这个错误会变成一个普通的全局未捕获异常,而 Promise 因为从没调用过resolve或reject,会一直停在 pending,挂在它上面的then和catch永远不会执行。正确写法是在异步回调里显式调用reject(err),或者在回调内部自己try...catch再reject。
容易答错的地方
- 以为外层
try...catch能抓住 try { new Promise(() => { throw err }) } catch (e) {}里的catch永远不会执行,因为错误已经被构造器转成拒绝了,没有从new表达式里抛出来。要处理只能用.catch(),或者在 async 函数里await这个 Promise 再try...catch。- 以为错误被捕获就等于被处理
- 构造器只是把「抛出」换成了「拒绝」。如果没有任何
catch处理这个 Promise,浏览器会在控制台打印Uncaught (in promise),Node.js 会触发unhandledRejection事件,较新版本默认直接让进程退出。 - 以为
throw一个字符串也能拿到err.message - 构造器会原样把抛出的值作为拒绝原因。
throw '出错了'时catch拿到的就是字符串,err.message是undefined,也没有调用栈。所以要throw new Error(...)。
面试官还会怎么问?
then 回调里 throw 会怎样?
和 executor 里一样,then 返回的新 Promise 会以这个错误拒绝,错误沿着链往后传,直到遇到第一个 catch 或带第二个参数的 then。
async 函数里同步 throw 呢?
也一样。async 函数体内任何位置抛出的错误都会被转成返回的 Promise 的拒绝,调用 async 函数本身不会抛错。所以调用时忘了 await 或 catch,错误就会变成未处理拒绝。
怎么把一个回调风格的异步 API 安全地包成 Promise?
在 executor 里调用这个 API,回调里根据是否有错误分别调用 reject(err) 和 resolve(data),不要在回调里 throw。Node.js 里可以直接用 util.promisify 帮你做这件事。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。