尽管达成共识,但未处理的 Promise 被拒绝

问题描述 投票:0回答:1

最近我偶然发现了一个有趣的错误。本质上,问题归结为这个例子:

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 有机会在滴答之间执行的事实会这样做。

但是,我真的很想听到比我更懂事的人给我一个正确的解释。

javascript node.js promise event-loop unhandled-promise-rejection
1个回答
0
投票

主机如何处理未处理的承诺拒绝取决于实现。 ECMAScript 规范说关于这一点

HostPromiseRejectionTracker 在两种场景下被调用:

  1. 当一个promise在没有任何处理程序的情况下被拒绝时,它会被调用,并将其操作参数设置为“reject”。
  2. 当第一次将处理程序添加到被拒绝的 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 中不允许),因为它已经中断了程序。

© www.soinside.com 2019 - 2024. All rights reserved.