I am trying to catch the rejection reason of an already rejected promise, but I am getting an uncaught error.
I have spent some time studying promises, but still do not understand why the error occurs.
The purpose of this question is to understand the technical (ECMAScript spec) reason for why the uncaught error happens.
Consider the following code:
const getSlowPromise = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Slow promise fulfillment value');
}, 1000);
});
};
const getFastPromise = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('Fast promise rejection reason');
}, 200);
});
};
const slowP = getSlowPromise();
const fastP = getFastPromise();
slowP
.then((v) => {
console.log(v);
console.dir(fastP);
return fastP;
}, null)
.then(
(v) => {
console.log(v);
},
(err) => {
console.log('Caught error >>> ', err);
},
);
In the above playground, the rejected promise is caught by the last then-handler (the rejection handler).
Inspecting the playground with DevTools will reveal that an error is thrown when fastP
rejects. I am not sure why that is the case.
This is my current understanding of what is going on under the hood, in the JS engine:
then(onFulfilled, onRejected)
. Let's call the promises then-promises, with onFulfilled
and onRejected
then-handlers.onFulfilled
and onRejected
then-handlers are saved in each then-promise's internal [[PromiseFulfillReactions]]
and [[PromiseRejectReactions]]
slots. That still happens synchronously, without touching the macrotask or microtask queuesfastP
rejects, reject('Fast promise rejection reason')
is added to the macrotask queue, which is passed to the call stack once empty and executed (after global execution context has finished)reject('Fast promise rejection reason')
runs on the call stack, it will set fastP
's internal [[PromiseState]]
slot to rejected
and the [[PromiseResult]]
to 'Fast promise rejection reason'
onRejected
handler it has saved in [[PromiseRejectReactions]]
to the microtask queue, which will be passed to the call stack and executed once the call stack is empty[[PromiseFulfillReactions]]
and [[PromiseRejectReactions]]
slots, as far as I understand. However, the first then-promise in the code snippet (created by the first then()
) does not yet know that it should be locked in to the fastP
promise, because that onFulfilled
then-handler has not run yet (then-promise mirrors promise returned from the corresponding then-handler).What am I missing? Is there a step where a connection is created between a then-promise and the returned promise from its then-handler, which only happens when the then-handler runs?
EDIT:
I understand why the rejection is caught, but not why an uncaught error first appears. I am looking for the technical explanation, to understand why it happens (see the last paragraph above).
This is the uncaught error the question relates to:
In Node.js
In DevTools in the SO playground (the snippet above)
the first then-promise in the code snippet (created by the first
then()
) does not yet know that it should be locked in to thefastP
promise, because that onFulfilledthen
-handler has not run yet
And that is exactly the problem. Due to this, no promise reactions are stored on fastP
- only on slowP
and on the first then-promise. The promise reactions on fastP
will only be created when the first then-promise is resolved with it (due to it being returned from the then
handler).
So in step 5, there are no [[PromiseRejectReactions]] on fastP
, and the unhandledrejection
/ unhandledRejection
event is fired.
According to the spec, this actually happens already in step 4, where RejectPromise calls the HostPromiseRejectionTracker as fastP
's [[PromiseIsHandled]] is still false
. It only gets set to true
when the promise is used.
Notice that as soon as slowP
resolves, the then
handler runs and returns fastP
, and the first then-promise is "locked in" to fastP
, the rejection of fastP
counts as handled and the HostPromiseRejectionTracker is called from fastP.then(resolve, reject)
to fire a rejectionhandled
/ rejectionHandled
on fastP
. This might further cause other unhandled rejections (on other promises, such as the first then-promise), though in your example you are handling that rejection. Your devtools may remove the error from the console (or modify it, maybe reducing it to a warning), nodejs will have already crashed the process (unless you caught the unhandledRejection
) so this can no longer happen.