先记住这个答案
当一个 Promise 变成 rejected,并且在当前这轮微任务全部执行完时,它身上还没有任何拒绝处理函数,浏览器就会在 window 上触发 unhandledrejection 事件,控制台里也会出现 Uncaught (in promise) 的报错。最常见的原因是 Promise 链末尾没有 catch、catch 挂在了错误的那个 Promise 上,或者调用 async 函数时既没有 await 也没有接 catch。避免的原则只有一条:每一条链的最后都要有人处理错误。
- 被拒绝且没有处理函数时触发,检查时机是微任务清空后
then会返回新 Promise,catch要挂在链的末尾- 调用
async函数不await也不catch,报错就没人接 - 全局监听只适合上报,不能代替正常的错误处理
最常见的三种漏处理写法
「处理」指的是给这个 Promise 挂上拒绝回调,包括 catch、then 的第二个参数,或者在 try 里 await 它。下面这些写法都会触发事件。
window.addEventListener('unhandledrejection', (event) => {
console.log('未处理的拒绝', event.reason)
// event.preventDefault() 可以阻止控制台打印默认报错
})
Promise.reject(new Error('没人接')) // 1. 直接拒绝,没有 catch
async function save() { throw new Error('保存失败') }
save() // 2. async 函数抛错变成被拒绝的 Promise,调用方没 await
Promise.resolve().then(() => { throw new Error('回调里抛错') }) // 3. then 返回的新 Promise 没人处理
// 三条都会触发 unhandledrejection第二种最容易漏。async 函数里 throw 不会像普通函数那样直接抛到调用方,而是让它返回的 Promise 变成 rejected。调用方如果只是 save() 一下,外面包再多层 try/catch 也接不住,因为 try 块早就同步执行完了。
catch 挂错位置
then 每调用一次都会返回一个新的 Promise。原 Promise 失败时,这个新 Promise 也会跟着失败,而它是一个独立的对象,需要单独处理。
then 和 catchJavaScriptconst p = request()
p.then(render) // 返回新 Promise,request 失败时它也失败
p.catch(showError) // 只处理了 p 本身
// request 失败时:showError 会执行,但 p.then(render) 那条链没人处理,照样触发
// 正确写法:request().then(render).catch(showError)这种写法看起来有 catch,实际上有两条链,只处理了一条。把 catch 接在整条链的最后,它就能同时接住 request 的失败和 render 里抛出的错误。
- 晚一点挂处理函数会怎样
- 浏览器是在微任务队列清空后检查的。如果先创建了一个被拒绝的 Promise,过了一个
setTimeout才去catch,那么unhandledrejection已经发出去了。等catch挂上后,浏览器会再派发一个rejectionhandled事件。监控系统如果只听前一个,就会多报一条其实被处理了的错误。
容易答错的地方
- 以为外层
try/catch能接住所有 Promise 错误 try/catch只能接住同步抛出的错误,以及在try里被await的 Promise 的拒绝。没有await的 Promise 是在try块结束后才失败的,这时已经出了try的范围。- 以为
Promise.all中其它失败的项也会报未处理 Promise.all会给每一项都挂上处理函数。第一个失败决定了结果,后面再失败的项已经被all内部处理过了,不会再触发事件。只要all返回的那个 Promise 有catch就够了。- 把全局监听当成错误处理
unhandledrejection适合做兜底上报,比如把event.reason发给 Sentry。它拿不到业务上下文,没法给用户正确的提示或者回滚状态。正常的错误处理还是要写在每个调用点上。
面试官还会怎么问?
Node.js 里未处理的拒绝会怎样?
Node.js 从 15 版本开始,默认行为是把未处理的拒绝当成未捕获异常,直接让进程退出。可以用 process.on('unhandledRejection', handler) 监听做日志,但更稳妥的做法是修掉漏处理的地方,而不是靠监听让进程活下来。
forEach 里写 async 回调,为什么错误接不住?
forEach 不会等回调返回的 Promise,也不会处理它们的拒绝。每个 async 回调失败时都是一个没人管的 Promise,外面 await 什么都等不到。要么改用 for...of 加 await,要么用 map 收集 Promise 再交给 Promise.all 统一处理。
故意不关心结果的 Promise,怎么避免报错?
明确地接一个空处理,比如 sendLog().catch(() => {}),同时最好在里面打一条日志。这样读代码的人能看出这是有意忽略,而不是忘了处理。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。