问题
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