先记住这个答案
catch 本质是 then(undefined, onRejected),它返回一个新的 Promise。放末尾时,它能捕获整条链上所有先于它产生的拒绝,并终止链;放中间时,它只捕获从链起点到该 catch 之前产生的拒绝,之后如果继续 then,链会被恢复,错误不再向外传递,但若后面的 then 又抛出异常,则需要再追加 catch 才能处理。
catch只捕获其之前的拒绝- 中间
catch后链恢复执行 - 末尾
catch兜住整链错误
catch 的链式传播规则
Promise.prototype.catch(onRejected) 等价于 then(undefined, onRejected),它返回一个新 Promise 并立即将其挂到原 Promise 上。如果原 Promise 已经变为 rejected,onRejected 会以该拒绝原因作为参数异步执行;若原 Promise 是 fulfilled,则跳过 onRejected,新 Promise 直接以原值兑现。
由此可知,catch 只关心其调用位置之前的拒绝,并且它的 onRejected 的返回值会成为新 Promise 的兑现值,从而把错误“吞掉”并让链后续走到 then 的成功回调。如果 onRejected 内部抛出异常或返回 rejected Promise,则新 Promise 再次被拒绝,需由后面的 catch 接住。
两个异步请求的失败场景
假设一个函数先请求用户资料,再请求用户订单,两个都是返回 Promise 的异步请求。当 catch 放在中间(后面还有 then)时,例如 fetchUser().then(fetchOrders).catch(err => { log(err); return defaultOrders; }).then(render),若 fetchUser 成功但 fetchOrders 失败,catch 捕获该失败并返回默认订单,后续 then(render) 会收到默认值并正常渲染;若 fetchUser 本身失败,同样被捕获。这样错误被就地处理,整条链继续运行。
若把 catch 放在末尾,即 fetchUser().then(fetchOrders).then(render).catch(err ⇒ { notify(err); }),则两个步骤中任一失败都会走到这个 catch,但 catch 之后没有任何 then 来恢复页面,只会通知用户,页面停留原状。要恢复,需要在 catch 中返回兜底数据或在前一个 then 中加内部 catch。
位置不是万能的边界条件
中间 catch 并非全能:它只能捕获当其被加入链时已经产生和后续依次传播的错误。若在 catch 之后的 then 回调里发生了异步错误(如 setTimeout 内 throw),该错误不会进入 Promise 链,catch 无法感知,会触发 unhandledrejection 或直接抛到全局。
为覆盖这种情况,应尽量把同步错误和 Promise 拒绝集中在链上,对真正的异步事件使用 async/await 配合 try/catch,或在回调内显式调用 reject。如果坚持使用链式,则需要在每个异步入口处将抛错转为 Promise 拒绝,例如包一个 new Promise 并把异常传给 reject,代价是增加样板代码。
容易答错的地方
- 认为 catch 放末尾能捕获所有错误
- 事实是,若 catch 是真正的链尾(后面没有任何 then),它确实能捕获其之前所有拒绝。但如果 catch 之后又接上了 then(即它不再是末尾),那么该 then 回调中抛出的新异常(同步 throw 或返回 rejected Promise)无法被这个 catch 捕获,需要再追加一个 catch 才能处理。
- 误以为 catch 会终结 Promise 链
- catch 返回一个 fulfilled Promise(除非 onRejected 抛错),所以链可以继续 then。很多人学到 catch 就认为错误被吞后停止,实际上它会恢复成正常值,这正是中间位置能提供降级逻辑的依据。
面试官还会怎么问?
catch 的 onRejected 中返回 rejected Promise,后续链会怎样?
返回的 rejected Promise 会让 catch 返回的新 Promise 变为 rejected,错误携带该 reason 继续向下。若后面没有 catch,会触发 unhandledrejection;若有,则被其捕获,类似把错误重新抛回链。
多个 catch 连续出现,错误会被哪个捕获?
错误只会被第一个遇到的 catch 捕获。第一个 catch 的 onRejected 得到机会处理,处理完并正常返回后,错误不会传给第二个 catch;如果第一个 catch 抛错或返回 rejected,才会流转到第二个。
catch 与 finally 的位置关系如何影响错误传播?
finally 不返回值只执行清理,若在本就有错误且无 catch 时,finally 执行后错误仍会继续传播。把 catch 放 finally 前可先处理错误,放 finally 后则先执行清理再捕获,但无论如何错误需至少一个 catch 避免未处理。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。