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:
- When the JS engine executes the script, it synchronously creates promise objects for each chained
then(onFulfilled, onRejected)
. Let’s call the promises then-promises, withonFulfilled
andonRejected
then-handlers. - The
onFulfilled
andonRejected
then-handlers are saved in each then-promise’s internal[[PromiseFulfillReactions]]
and[[PromiseRejectReactions]]
slots. That still happens synchronously, without touching the macrotask or microtask queues - When
fastP
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) - When
reject('Fast promise rejection reason')
runs on the call stack, it will setfastP
‘s internal[[PromiseState]]
slot torejected
and the[[PromiseResult]]
to'Fast promise rejection reason'
- It will then add any
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 - Here is where I fall off. The then-promises we discussed previously do have handlers saved in their
[[PromiseFulfillReactions]]
and[[PromiseRejectReactions]]
slots, as far as I understand. However, the first then-promise in the code snippet (created by the firstthen()
) does not yet know that it should be locked in to thefastP
promise, because thatonFulfilled
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)
3
Answers
Basically slowP and fastP not related to each other in any sense before you return fastP. So yes, there’s no any handler to handle fastP in the first place (you attach a handler after the rejection) so that’s why the error appears. Anything after
doesn’t matter at all, no any connection to this error. Chrome though removes this error from the console after you handle fastP with a callback. Firefox leaves the message, probably Node.js also.
Correct way…
And that is exactly the problem. Due to this, no promise reactions are stored on
fastP
– only onslowP
and on the first then-promise. The promise reactions onfastP
will only be created when the first then-promise is resolved with it (due to it being returned from thethen
handler).So in step 5, there are no [[PromiseRejectReactions]] on
fastP
, and theunhandledrejection
/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 stillfalse
. It only gets set totrue
when the promise is used.Notice that as soon as
slowP
resolves, thethen
handler runs and returnsfastP
, and the first then-promise is "locked in" tofastP
, the rejection offastP
counts as handled and the HostPromiseRejectionTracker is called fromfastP.then(resolve, reject)
to fire arejectionhandled
/rejectionHandled
onfastP
. 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.