更新了,我现在尝试了explaining the behavior I'm seeing,但是从可靠来源获得关于 unhandledRejection 行为的答案仍然很棒。我还在Reddit上开始了discussion thread的发布。

为什么在以下代码中出现unhandledRejection事件(“错误f1”)?这是出乎意料的,因为我在finallymain部分处理了两个拒绝。
我在Node(v14.13.1)和Chrome(v86.0.4240.75)中看到了相同的行为:

window.addEventListener("unhandledrejection", event => {
  console.warn(`unhandledRejection: ${event.reason.message}`);
});

function delay(ms) {
  return new Promise(r => setTimeout(r, ms));
}

async function f1() {
  await delay(100);
  throw new Error("error f1");
}

async function f2() {
  await delay(200);
  throw new Error("error f2");
}

async function main() {
  // start all at once
  const [p1, p2] = [f1(), f2()];
  try {
    await p2;
    // do something after p2 is settled
    await p1;
    // do something after p1 is settled
  }
  finally {
    await p1.catch(e => console.warn(`caught on p1: ${e.message}`));
    await p2.catch(e => console.warn(`caught on p2: ${e.message}`));
  }
}

main().catch(e => console.warn(`caught on main: ${e.message}`));

最佳答案

好吧,对我自己说。我误解了 unhandledrejection 事件实际上是如何工作的。
我来自.NET,那里的Task对象can remain unobserved失败,直到被垃圾回收。如果仍然无法执行任务,则只有 UnobservedTaskException 会被触发。
JavaScript promise 的情况有所不同。尚未附加拒绝处理程序(通过Promisethencatchawait)的拒绝的Promise.all/race/allSettle/any需要尽快安装,否则可能会触发unhandledrejection事件。
什么时候会精确触发unhandledrejection?这似乎确实是特定于实现的。当用户代理通知已拒绝的 promise 时,W3C会在“未处理的 promise 拒绝” do not strictly specify上进行规范。
为了安全起见,在当前函数将执行控制放弃给调用者之前,我将同步附加处理程序(通过类似returnthrowawaityield的操作)。
例如,以下代码不会触发unhandledrejection,因为await延续处理程序是在p1 promise被创建为已经被拒绝后立即同步附加到p1的。那讲得通:

window.addEventListener("unhandledrejection", event => {
  console.warn(`unhandledRejection: ${event.reason.message}`);
});

async function main() {
  const p1 = Promise.reject(new Error("Rejected!"));
  await p1;
}

main().catch(e => console.warn(`caught on main: ${e.message}`));

即使我们异步将unhandledrejection处理程序附加到await,以下代码也不会触发p1。我只能推测,这可能是发生的,因为已解决的promise的延续被发布为microtask:

window.addEventListener("unhandledrejection", event => {
  console.warn(`unhandledRejection: ${event.reason.message}`);
});

async function main() {
  const p1 = Promise.reject(new Error("Rejected!"));
  await Promise.resolve();
  await p1;
}

main().catch(e => console.warn(`caught on main: ${e.message}`));

带有浏览器行为的Node.js(发布时为v14.14.0)is consistent
现在,以下确实触发了unhandledrejection事件。再次,我可以推测是因为现在在处理task (macrotask)队列时,await延续处理程序现在异步附加到p1上,并且在事件循环的某些后续迭代中:

window.addEventListener("unhandledrejection", event => {
  console.warn(`unhandledRejection: ${event.reason.message}`);
});

async function main() {
  const p1 = Promise.reject(new Error("Rejected!"));
  await new Promise(r => setTimeout(r, 0));
  await p1;
}

main().catch(e => console.warn(`caught on main: ${e.message}`));

我个人觉得这整个行为令人困惑。我喜欢.NET方法来更好地观察Task结果。我可以想到许多情况,当我真正想要保留对promise的引用,然后对其进行await编码,并在以后的时间轴上捕获与其解决或拒绝有关的任何错误。
就是说,有一个简单的方法可以在不引起unhandledrejection事件的情况下获得此示例的所需行为:

window.addEventListener("unhandledrejection", event => {
  console.warn(`unhandledRejection: ${event.reason.message}`);
});

async function main() {
  const p1 = Promise.reject(new Error("Rejected!"));
  p1.catch(console.debug); // observe but ignore the error here
  try {
    await new Promise(r => setTimeout(r, 0));
  }
  finally {
    await p1; // throw the error here
  }
}

main().catch(e => console.warn(`caught on main: ${e.message}`));

09-18 08:29