Promise 出现意外的 unhandledRejection 事件,而拒绝确实得到了处理

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

更新了,我现在已经尝试解释我所看到的行为,但如果能从可靠的来源获得关于

unhandledRejection
行为的答案仍然很棒。我还在 Reddit 上发起了讨论帖


为什么我在以下代码中收到

unhandledRejection
事件(“错误 f1”)?这是出乎意料的,因为我在
finally
main
部分处理了两次拒绝。

我在 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}`));

javascript node.js promise async-await es6-promise
4个回答
14
投票

好吧,回答我自己。我误解了

unhandledrejection
事件的实际运作方式。

我来自.NET,其中失败的

Task
对象可以保持不被观察到,直到它被垃圾收集。只有这样,如果任务仍未被观察到,
UnobservedTaskException
才会被解雇。

JavaScript Promise 的情况有所不同。被拒绝的

Promise
没有 已附加拒绝处理程序(通过
then
catch
await
Promise.all/race/allSettle/any
),需要一个 尽早,否则
unhandledrejection
事件 可能被解雇。

什么时候

unhandledrejection
会被解雇(如果有的话)?这似乎确实是特定于实现的。关于“未处理的承诺拒绝”的 W3C 规范没有严格指定用户代理何时通知被拒绝的承诺

为了安全起见,我会在当前函数将执行控制权交给调用者之前同步附加处理程序(通过类似

return
throw
await
yield
)。

例如,以下代码不会触发

unhandledrejection
,因为
await
延续处理程序会在已拒绝状态下创建
p1
承诺之后同步附加到
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
。我只能推测,这可能会发生,因为已解决的承诺的延续被发布为微任务

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

async function main() {
  const p1 = Promise.reject(new Error("Rejected!")); 
  await Promise.resolve(r => queueMicrotask(r));
  // or we could just do: await Promise.resolve();
  await p1;
}

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

Node.js(发布本文时为 v14.14.0)与浏览器行为一致

现在,以下 触发

unhandledrejection
事件。同样,我可以推测这是因为
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
结果。我可以想到很多情况,当我真的想保留对承诺的引用,然后
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}`));


2
投票

您应该使用

try...catch
来捕获
try
块内发生的所有错误:

try {
    await p2;
    // do something after p2 is settled
    await p1;
    // do something after p1 is settled
  }
catch(e) {
  // do something with errors e
}

编辑:

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 main() {
  try {
  const p1 = await f1();
  await delay(200);
  }
  catch(e) {
    console.warn(`caught inside main: ${e.message}`);
  }
}

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


0
投票

我没有来源,但我认为它的工作原理是这样的:

Promise.reject(new Error("Rejected!"));
返回一个被拒绝的承诺,该承诺将在下一个蜱虫发生错误。 所以:

async function main3() {
    //this wil throw the error next tick
    const p1 = Promise.reject(new Error("Rejected!")); 
    //this will run immediately and attach the await to the promise (so it will not be rejected)
    await p1;
}

然后

Promise.resolve
会将其结果返回给所有
.then
处理程序下一个刻度(我们没有它们,因为不会将结果存储在任何地方) 所以:

async function main() {
    //this wil throw the error next tick
    const p1 = Promise.reject(new Error("Rejected!")); 
    //this will run immediately (and would give its value next tick)
    await Promise.resolve();
    //then this will run immediately and attach the await to the promise
    await p1;
}

最后,延迟为 0 的

setTimeout
不会立即触发,请检查:https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop 并阅读 0 延迟部分 所以:

async function main2() {
    //this wil throw the error next tick
    const p1 = Promise.reject(new Error("Rejected!"));  
    //setTimeout does with 0 does run not immediately.
    //because of this the await p1 does not get added before the promise is rejected
    await new Promise(r => setTimeout(r, 0));
    //so this does nothing and the prosime will reject
    await p1;
}

0
投票

基本上,在 JavaScript 中,如果 Promise 被拒绝并且在事件循环的一轮内不知道错误处理,会发生什么 -

unhandledRejection
事件将被触发,但如果您在下一个循环周期中处理该事件,稍后会发生另一个事件
rejectionHandled
也会被解雇。

因此可以得出结论,当异步代码错误处理在事件循环的轮次中未知时发出的

unhandledRejection
不一定是错误,有时错误处理发生在事件循环的非常不同的轮次。

const mp = new Map();

let p = new Promise((resolve, reject)=>{
     if(1 > 2){
          resolve(true)
     }
     else{
          reject("opps!");
     }
});

function pp(pr:Promise<any>){
     return pr.then((val)=>{
          console.log(val);
     });
}

function main(){
     let ret = pp(p);

     setTimeout(()=>{
          ret.catch(err => {
               console.log(err)
          })
     }, 2000)
}

main()


process.on("unhandledRejection", (re, pr)=>{
     mp.set(pr, re);
})

process.on("rejectionHandled", (pr)=>{
     console.log(mp);
     console.log(mp.delete(pr));
})
© www.soinside.com 2019 - 2024. All rights reserved.