最近我偶然发现了一个有趣的错误。本质上,问题归结为这个例子:
const waitResolve = (ms) => new Promise((resolve) => {
setTimeout(() => {
console.log(`Waited to resolve for ${ms}ms`);
resolve(ms);
}, ms);
});
const waitReject = (ms) => new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Waited to reject for ${ms}ms`);
reject(ms);
}, ms);
});
const run = async() => {
const promises = {
a: [],
b: [],
c: [],
};
for (let i = 0; i < 5; i += 1) {
promises.a.push(waitResolve(1e4));
promises.b.push(waitReject(1e3));
promises.c.push(waitResolve(1e2));
}
try {
for (const [key, value] of Object.entries(promises)) {
console.log(`Starting ${key}`);
try {
await Promise.all(value);
} catch (err) {
console.log(`Caught error in ${key}!`, err);
}
console.log(`Finished ${key}`);
}
} catch (err) {
console.log('Caught error in run!', err);
}
};
run();
在这里,尽管普遍的理解是承诺将在
for
循环期间和之后处于挂起状态,并且只有在 Promise.all
调用之后才会“真正”开始执行。这意味着 try/catch
块将捕获 waitReject(1e3)
中产生的拒绝,但它不会发生(在 Node.js v18.14.2 和几个早期版本中测试)。
如果 Promises 数组推送的顺序更改为:
promises.a.push(waitResolve(1e2));
promises.b.push(waitReject(1e3));
promises.c.push(waitResolve(1e4));
拒绝会被抓住。现在,我确实模糊地理解它与事件循环内的 mIcro 和 mAcro 任务解析序列有关,并且 Promise 有机会在滴答之间执行的事实会这样做。
但是,我真的很想听到比我更懂事的人给我一个正确的解释。
主机如何处理未处理的承诺拒绝取决于实现。 ECMAScript 规范说关于这一点:
HostPromiseRejectionTracker 在两种场景下被调用:
- 当一个promise在没有任何处理程序的情况下被拒绝时,它会被调用,并将其操作参数设置为“reject”。
- 当第一次将处理程序添加到被拒绝的 Promise 时,将调用它,并将其操作参数设置为“handle”。
HostPromiseRejectionTracker 的典型实现可能会尝试通知开发人员未处理的拒绝,同时还要小心地通知他们,如果此类先前的通知后来因附加的新处理程序而失效。
我注意到在 NodeJs 中,这个处理程序会停止脚本并且不允许程序继续,因此循环的第二次迭代永远不会发生。然而,在 Chrome/Edge 中,控制台首先显示未捕获的承诺拒绝错误,但异步代码可以继续,并且一旦处理承诺拒绝,这些错误消息就会从控制台中删除。这显然是 ECMAScript 规范没有规定的两种不同方法:由主机决定要做什么。
至于你的分析:
在这里,尽管普遍认为承诺 [...] 仅在
调用之后才“真正”开始执行。
Promise.all
开始执行的并不是承诺。 Promise 只是对象,而不是函数。然而,
setTimeout
计时器在创建promise的那一刻就开始了,即在第二个循环之前。 Promise.all
的效果是不是某些promise被“执行”,而是创建一个新的promise,当所有给定的promise解决时,该新的promise保证能够解决,或者一旦其中一个被拒绝,它就会被拒绝。因此,它在这些承诺上放置了处理程序,并且 try ... catch
块充当新的“所有”承诺的拒绝处理程序。
真正打开状态变化之门的是
await
。这将使 async
函数返回。当调用堆栈为空时,将监视任务队列。当 setTimeout
过期时,那里会出现一个任务,该任务将运行您的代码来解析/拒绝承诺。
如果这是一个拒绝,并且该 Promise 尚未收到拒绝处理程序,则预计将触发未捕获的拒绝错误。由于循环第一次迭代中的
await
仅在“a”承诺上放置了处理程序,因此拒绝“b”承诺将导致未捕获的拒绝处理程序错误。这是预料之中的。只有当所有“a”承诺都已解决并且循环可以进行第二次迭代时,才会为“b”承诺设置拒绝处理程序。 Nodejs 不允许这种情况发生(至少在我运行的版本 - v20 中不允许),因为它已经中断了程序。