先记住这个答案
catch 本质上是 then(undefined, onRejected),它和 then 一样会返回一个新的 Promise。放在中间时,它处理完错误就让链恢复成功,后面的 then 照常执行,拿到的是 catch 回调的返回值。放在末尾时,前面任何一步出错,中间剩下的 then 都会被跳过,错误直接落到这个 catch。所以中间的 catch 是局部兜底,末尾的 catch 是整条链的保险。
- 中间的
catch吃掉错误,后面的then照常执行 - 末尾的
catch兜住整条链的错误 catch没有return,后续拿到undefined- 想继续失败,就在
catch里重新throw
两种位置的对比
同样是第一步就失败,只是 catch 的位置不同,后面那一步的命运完全不一样。
catch 和末尾 catchJavaScriptfunction middle() {
return Promise.reject(new Error('A 失败'))
.catch((e) => console.log('中间 catch:', e.message)) // 没有 return,相当于返回 undefined
.then((v) => console.log('B 照常执行,拿到', v)) // 链已经恢复成功
}
function tail() {
return Promise.reject(new Error('A 失败'))
.then(() => console.log('B 被跳过')) // 链处于失败状态,成功回调不执行
.catch((e) => console.log('末尾 catch:', e.message))
}
middle().then(tail) // 先跑完 middle 再跑 tail,输出不交错查看输出与解释
中间 catch: A 失败
B 照常执行,拿到 undefined
末尾 catch: A 失败middle 里,catch 把错误处理掉了,而且回调正常返回,于是它返回的 Promise 变成成功状态,后面的 then 就执行了,拿到 undefined。
tail 里,B 那一步的 then 只注册了成功回调,链是失败状态,它就原样把错误往下传,一直传到末尾的 catch。
可以把 Promise 链想成一条流水线,失败就是一件次品在往下走。每个 then 只处理合格品,看到次品直接放行;catch 是质检员,把次品拦下来修好,放回流水线上继续走。质检员站在哪,决定了次品在哪一站被拦下。
catch 的返回值决定后面怎么走
catch 回调怎么结束,决定了它返回的那个 Promise 是什么状态,这是理解位置差异的关键。
- 三种结束方式
- 正常
return一个值,比如return [],链恢复成功,后面的then拿到这个值。这常用来给失败的请求一个默认数据,让页面照常渲染。 什么都不返回,后面拿到undefined。上面例子就是这种情况,后面的then如果去读v.length,就会抛出TypeError,很多「错误处理了但页面还是报错」的 bug 就出在这里。 在catch里throw,或者return Promise.reject(err),链继续保持失败,错误交给下一个catch。常见用法是中间的catch只负责上报日志,然后把错误重新抛出,让末尾的catch统一给用户提示。
容易答错的地方
- 以为中间
catch之后,后面的then就不执行了 - 正好相反。
catch正常返回后链就恢复成功了,后面的then一定会执行。如果不希望后续逻辑继续跑,要么在catch里重新throw,要么把catch移到末尾。 - 以为
then的第二个参数和后面接catch完全一样 p.then(onOk, onErr)里,onErr只能接住p的错误,接不住onOk自己抛出的错误。p.then(onOk).catch(onErr)则两种错误都能接住,因为catch挂在then返回的新 Promise 上。- 以为末尾有
catch就万事大吉 catch只管它前面的部分。如果catch后面又接了then,或者catch回调自己抛了错,这些错误没有下一个catch接,就会变成未处理的拒绝,浏览器会触发unhandledrejection事件。
面试官还会怎么问?
链上有多个 catch,错误会被处理几次?
只会被第一个遇到的 catch 处理一次。第一个 catch 正常返回后链就恢复成功了,后面的 catch 不会执行。除非第一个 catch 重新 throw,错误才会继续传到下一个。
写成 async/await 时,catch 的位置对应什么?
对应 try/catch 包住的范围。只包一行 await,相当于中间的 catch,出错后函数会接着往下执行;把整个函数体都包进去,相当于末尾的 catch,任何一步出错都跳到 catch 块。
实际项目里中间 catch 一般用在哪?
用在「这一步失败了也能继续」的地方。比如页面同时要加载推荐列表,推荐接口挂了不应该让整页白屏,就在请求推荐的那一步后面接 catch(() => []),返回空数组,后面的渲染照常进行。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。