Promise同步reject内部机制及未处理拒绝检测逻辑疑问
问题背景
我原本以为executor函数里的reject方法是这么工作的:把Promise状态设为rejected,然后把所有通过.catch或.then第二个参数注册的回调推入微任务队列;要是找不到任何回调就直接抛出错误。但实际测试后发现这个理解错了,看下面的代码片段:
代码1
const p = new Promise((resolve, reject) => reject(9));
这段代码会抛出未处理异常错误。
代码2
const p = new Promise((resolve, reject) => reject(9)); p.catch(e => console.log(e));
这段代码却不会报错,反而输出9。按我之前的错误理解,代码2里Promise是立即执行的,executor同步调用reject的时候还没注册错误处理函数,应该抛出错误且p.catch不会执行,但实际结果和预期完全不一样。
reject的内部工作机制到底是怎样的?
补充疑问
看到评论和回答说,未处理拒绝的检测要等到控制权回到事件循环后才进行,这时候如果还没有错误处理函数就抛出错误。但看下面这段代码:
const p = new Promise ((resolve, reject) => reject("bad")); setTimeout(() => { p.catch((e) => { console.log(e + " caught") }); }, 0)
Promise被拒绝后,setTimeout执行,控制权回到事件循环,此时定时器回调已经推入回调队列但还没执行,按道理这时候没有错误回调,应该抛出未处理异常,但实际输出bad caught。这是为什么?
解答
1. reject的核心工作流程
reject的核心逻辑其实分两步:
- 第一步:同步变更状态:立刻把Promise的状态从pending改成rejected,同时保存拒绝的原因。
- 第二步:延迟处理回调:它不会在调用时立刻检查有没有错误回调,而是会记住当前的rejected状态。之后不管是调用reject之前还是之后注册的
.catch/.then第二个参数回调,只要Promise已经是rejected状态,就会把这些回调推入微任务队列等待执行。
2. 未处理拒绝的检测时机
浏览器和Node.js的未处理Promise拒绝检测机制,绝对不是在调用reject的时候立刻触发,而是遵循这样的逻辑:
- 当Promise被reject后,先等当前的同步代码块完全执行完。
- 进入事件循环的微任务阶段,把所有微任务队列里的任务都执行完毕。
- 只有在微任务清空之后,引擎才会检查这个rejected的Promise有没有绑定对应的错误处理回调。如果这时候还没有任何回调,才会抛出未处理异常错误。
回到代码2的场景:
const p = new Promise((resolve, reject) => reject(9)); // 同步把状态改成rejected p.catch(e => console.log(e)); // 此时Promise已经是rejected状态,直接把catch回调推入微任务队列
同步代码执行完后,微任务队列里的catch回调被执行,输出9。引擎检测到这个Promise已经有错误处理了,自然不会抛出异常。
3. 补充代码的原因解析
对于补充的那段代码,关键在于未处理拒绝的检测时机不是“控制权刚回到事件循环”的那一刻,而是在每一轮事件循环的微任务阶段结束之后。具体流程是这样的:
- 同步代码执行:Promise被reject,状态变成rejected;
setTimeout的回调被推入宏任务队列。 - 当前同步代码块执行完毕,进入第一轮事件循环的微任务阶段:这时候没有微任务,队列清空。
- 引擎开始检查未处理的Promise拒绝,但现代JS环境会给一个“窗口期”——允许开发者在后续的宏任务里绑定错误回调。不同环境的检测逻辑略有不同,但普遍不会立刻判定为“未处理”,而是会等至少一轮宏任务执行前,或者只要在全局未处理拒绝的监听触发前绑定了回调,就不会报错。
- 进入宏任务阶段,执行
setTimeout的回调:这时候绑定了p.catch,回调被推入微任务队列并执行,输出bad caught。 - 微任务执行完毕后,引擎再次检查,发现这个Promise已经有错误处理了,所以不会抛出异常。
说白了,现代JS环境不会在Promise刚被reject且同步代码结束后就马上判定为“未处理”,而是会给你机会在后续的事件循环阶段(哪怕是宏任务)绑定回调,避免误判。
内容的提问来源于stack exchange,提问作者Urooj

