Unexpected unhandledRejection event for promise which rejection does get handled

痴心易碎 提交于 2020-12-02 08:12:42

问题


Updated, I've now tried explaining the behavior I'm seeing, but it'd still be great to have an answer from a credible source about the unhandledRejection behavor. I've also started a discussion thread on Reddit.


Why do I get an unhandledRejection event (for "error f1") in the following code? That's unexpected, because I handle both rejections in the finally section of main.

I'm seeing the same behavior in Node (v14.13.1) and 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}`));

回答1:


You should be using try...catch to catch all the errors happening inside your try block:

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

EDIT:

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



回答2:


Ok, answering to myself. I misunderstood how unhandledrejection event actually works.

I'm coming from .NET where a failed Task object can remain unobserved until it gets garbage-collected. Only then UnobservedTaskException will be fired, if the task is still unobserved.

Things are different for JavaScript promises. A rejected Promise that does not have a rejection handler already attached (via then, catch, await or Promise.all/race/allSettle/any), needs one as early as possible, otherwise unhandledrejection event may be fired.

When unhandledrejection will be fired exactly, if ever? This seems to be really implementation-specific. The W3C specs on "Unhandled promise rejections" do not strictly specify when the user agent is to notify about rejected promises.

To stay safe, I'd attach the handler synchronously, before the current function relinquishes the execution control to the caller (by something like return, throw, await, yield).

For example, the following doesn't fire unhandledrejection, because the await continuation handler is attached to p1 synchronously, right after the p1 promise gets created in already rejected state. That makes sense:

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

The following still does not fire unhandledrejection, even though we attach the await handler to p1 asynchronously. I could only speculate, this might be happening because the continuation for the resolved promised is posted as a 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 at the time of posting this) is consistent with the browser behavior.

Now, the following does fire the unhandledrejection event. Again, I could speculate that's because the await continuation handler is now attached to p1 asynchronously and on some later iterations of the event loop, when the task (macrotask) queue is processed:

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

I personally find this whole behavior confusing. I like the .NET approach to observing Task results better. I can think of many cases when I'd really want to keep a reference to a promise and then await it and catch any errors on a later timeline to that of its resolution or rejection.

That said, there is an easy way to get the desired behavior for this example without causing unhandledrejection event:

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



回答3:


I do not have a source but i think it works like this: The Promise.reject(new Error("Rejected!")); returns a rejected promise that will error next tick. so:

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;
}

Then Promise.resolve will return its result to all .then handler next tick (we dont have them as the wont store the result anywere) so:

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;
}

Lastly a setTimeout with 0 delay does not immediately trigger, check: https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop and read the 0 delay section so:

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;
}


来源:https://stackoverflow.com/questions/64336083/unexpected-unhandledrejection-event-for-promise-which-rejection-does-get-handled

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!