先记住这个答案
then 回调抛出的异常会被 Promise 机制捕获,使 then 返回的新 Promise 进入 rejected 状态。因此,异常会沿着链向下传播,直到遇到 catch 或拒绝处理回调。若链末端没有捕获,会产生未处理的拒绝。捕获范围仅限该回调及其内部同步操作,异步操作中抛出的异常不会自动捕获。
- 回调抛异常自动转为拒绝
- 后续 then 的成功回调不执行
- 未捕获则触发 unhandledrejection
then 返回新 Promise 的决议机制
调用 promise.then(onFulfilled, onRejected) 时,会立即返回一个全新的 Promise(记为 p2)。当原始 Promise 变为 fulfilled 后,JavaScript 会将 onFulfilled 放入微任务队列。执行该回调时,如果它抛出异常,这个异常会被隐式捕获,并作为 p2 的拒绝原因,使 p2 直接进入 rejected 状态。这一机制与 Promise 构造器中 executor 抛错相似,但发生在 then 中。
若 onFulfilled 正常返回一个值(包括 undefined),p2 会以该值被 resolve;若返回一个 thenable 或 Promise,p2 会等待它 settle 后再决议。抛出异常相当于返回一个 rejected Promise,因此链上的下一个 then 若没有提供 onRejected,就会跳过其成功回调,直到遇到 catch 或同级的失败处理。这种设计保证错误不会丢失,会沿链传递。
Promise.resolve(1)
.then(v => {
console.log('first then, value:', v);
throw new Error('boom');
})
.then(() => console.log('second then fulfilled'))
.catch(err => console.log('caught:', err.message));
// 输出:
// first then, value: 1
// caught: boom查看输出与解释
first then, value: 1
caught: boom第二个 then 不会执行,因为第一个 then 抛出的异常使链变为 rejected,直接跳到 catch。
读取用户配置时的错误中断流程
假设从服务端获取用户配置后,需要解析 JSON 并校验必填字段。代码写成 fetchConfig().then(JSON.parse).then(validate).then(render)。若 fetchConfig 成功但服务器返回畸形 JSON,JSON.parse 会抛同步错误,该错误自动使第二个 then 返回的 Promise 变为 rejected,从而跳过 validate 和 render,直接进入链尾的 catch。这样避免了无效数据继续流向界面。
若在 validate 中又发现字段缺失而主动抛出 ValidationError,同样会继续沿链走到同一 catch。因此,链式 then 中任何一步的同步异常都会中断后续所有成功回调。若需要针对不同错误做不同处理,可链上插入多个 catch 以细分,但注意只有 catch(或 then 的 onRejected 回调)可以恢复链。
异常捕获仅作用于同步部分
如果回调内部启动了异步操作(如 setTimeout),在异步回调中抛出的异常不会被 Promise 捕获。例如 Promise.resolve().then(() => setTimeout(() => { throw new Error('later'); })),这个异常直接成为全局未捕获异常,不会影响该 then 返回的 Promise,因为此时同步回调已结束,Promise 已决议。因此,要在异步任务中传播错误,应返回一个 Promise,并让该 Promise 在异步失败时被拒绝,而不是在异步回调中直接 throw。
另外,若 then 回调中调用 reject 风格的函数是不存在的,因为 Promise 是构造器中的参数。正确的处理是返回一个 rejected Promise,如 return Promise.reject(new Error()),效果等同抛异常。但两种方式在链上处理相同。还需注意不应混淆 try/catch 的作用,Promise 的内部捕获机制已自动处理同步异常,无需手工包裹。
容易答错的地方
- 认为异常会冒泡到外层 try/catch
- 有人以为 then 回调中抛错能被外部 try/catch 捕获。实际上,then 回调在微任务中执行,已脱离同步调用栈,外层 try/catch 无法捕获,必须使用 Promise 的拒绝处理机制。
- 混用异步抛错与同步 throw
- 认为在 then 回调里写 setTimeout 内 throw 也会被 Promise 捕获。错,Promise 只捕获回调同步执行的异常,异步回调中的异常不会影响链的状态,只会成为全局错误。
面试官还会怎么问?
如何在 then 回调中主动产生一个拒绝而不使用 throw?
可以返回一个 rejected Promise,如 return Promise.reject(new Error())。这样效果与 throw 相同,但更显式,便于表达意图,且利于链式流程控制。
如果 then 回调抛出的异常是一个字符串而非 Error 对象,链后续能正确处理吗?
可以。Promise 会将该值作为拒绝原因,不影响状态转变。但抛出字符串会使错误堆栈缺失,建议始终抛 Error 对象以便调试。
then 回调中抛出异常与调用 onRejected 后抛出异常有何不同?
无论哪个回调抛出异常,都会使 then 返回的新 Promise 变为 rejected。因此,如果 onRejected 本身抛错,会继续沿链寻找下一个 catch,这可能导致原始错误被覆盖。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。