先记住这个答案
then 调用后会返回一个新的 Promise。回调执行时如果抛出异常,这个异常会被 Promise 自动捕获,新 Promise 变成 rejected,异常就是拒绝原因。于是后面的 then 成功回调都会被跳过,错误一路往下传,直到遇到 catch 或 then 的第二个参数。如果整条链都没有处理,就会变成未处理的拒绝。要注意,只有回调里同步抛出的异常才会被捕获。
then回调里throw等于返回一个拒绝的 Promise- 后续
then的成功回调全部跳过,直达catch - 回调里异步抛出的错误接不住
- 没有
catch会触发unhandledrejection
错误怎么沿链走下去
先看一条最常见的链:第一步抛错,看看第二步和 catch 之后会发生什么。
Promise.resolve(1)
.then((v) => {
console.log('第一个 then:', v)
throw new Error('boom') // 效果等同于 return Promise.reject(new Error('boom'))
})
.then(() => console.log('第二个 then')) // 被跳过
.catch((err) => {
console.log('catch:', err.message)
return 'fallback' // 正常返回,链恢复成功
})
.then((v) => console.log('catch 之后:', v))查看输出与解释
第一个 then: 1
catch: boom
catch 之后: fallback第一个 then 抛错后,它返回的 Promise 以这个 Error 被拒绝。第二个 then 只给了成功回调,拿到的却是一个拒绝,于是它什么也不做,直接把同样的错误传给自己返回的 Promise。catch 接住错误,正常返回了 'fallback',链又恢复成成功,最后一个 then 拿到这个值。
可以理解为每个 then 回调都被 Promise 悄悄套了一层 try/catch:回调正常返回,就用返回值兑现新 Promise;回调抛错,就用错误拒绝新 Promise。这也是为什么 throw err 和 return Promise.reject(err) 在链上效果一样。
真实场景:一步出错,后面全部跳过
比如读取用户配置,链写成 loadConfig().then(JSON.parse).then(validate).then(render).catch(showError)。
- 哪一步出错都走同一个出口
- 服务器返回的内容不是合法 JSON,
JSON.parse就会抛出SyntaxError。这个异常让那一步的 Promise 被拒绝,validate和render都被跳过,直接进入showError。validate发现必填字段缺失时,主动throw new Error('缺少 theme 字段'),也走同一个出口。 这就是链式写法的好处:你不需要在每一步检查上一步有没有成功,坏数据不会流到界面上,所有错误集中在末尾处理。
哪些错误接不住
自动捕获有一个边界:它只管回调执行期间同步抛出的异常。
throwJavaScriptPromise.resolve()
.then(() => {
setTimeout(() => {
throw new Error('later') // 这时 then 回调早已返回,新 Promise 已经兑现
}, 0)
})
.catch(() => console.log('接不住')) // 不会执行,错误变成全局未捕获异常then 回调执行时只是注册了一个定时器,然后正常返回 undefined,新 Promise 立刻就兑现了。一段时间后定时器里的 throw 发生在另一个调用栈里,跟这条链毫无关系,只会变成全局的未捕获异常。
正确的做法是把异步操作包成 Promise 并 return 出去,在里面失败时调用 reject,这样链才会等待它,失败也能被 catch 接到。
容易答错的地方
- 以为外层
try/catch能接住then里的错误 then回调是在微任务里执行的,那时外层的try块早就执行完了。这种写法接不住任何东西,要么在链上加catch,要么改成await放在try里面。- 以为抛错之后后面的
then都不会执行了 - 只是成功回调被跳过。一旦错误被
catch处理掉并正常返回,后面的then又会照常执行,拿到的是catch的返回值。
面试官还会怎么问?
不用 throw,怎么在 then 里主动让链失败?
返回一个拒绝的 Promise,也就是 return Promise.reject(new Error('xxx'))。效果和 throw 一样。一般更推荐直接 throw,写法短,而且在 async 函数里也是同样的写法。
throw 一个字符串而不是 Error 对象,行不行?
能正常传递,catch 拿到的就是这个字符串。但字符串没有调用栈,出了问题很难定位是哪一行抛的,所以约定上始终抛 Error 或它的子类。
catch 回调里又抛了错会怎样?
catch 返回的 Promise 同样会被拒绝,新的错误继续往下找下一个 catch。如果后面没有了,就是一个未处理的拒绝,而且原来那个错误的信息被新错误覆盖了,排查时要特别留意这种情况。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。