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}`));
If an error condition arises inside a promise, you “reject” the promise by calling the reject() function with an error. To handle a promise rejection, you pass a callback to the catch() function. This is a simple example, so catching the rejection is trivial.
The unhandledrejection event is sent to the global scope of a script when a JavaScript Promise that has no rejection handler is rejected; typically, this is the window , but may also be a Worker . This is useful for debugging and for providing fallback error handling for unexpected situations.
You can handle rejected promises without a try block by chaining a catch() handler before awaiting the promise.
A Promise rejection indicates that something went wrong while executing a Promise or an async function. Rejections can occur in several situations: throwing inside an async function or a Promise executor/then/catch/finally callback, when calling the reject callback of an executor , or when calling Promise.
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(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 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}`));
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}`));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With