skip to Main Content

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:

  1. 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, with onFulfilled and onRejected then-handlers.
  2. The 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 queues
  3. 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)
  4. When 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'
  5. 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
  6. 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 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

enter image description here

In DevTools in the SO playground (the snippet above)

enter image description here

3

Answers


  1. 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

    const slowP = getSlowPromise();
    const fastP = getFastPromise();
    

    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.

    Login or Signup to reply.
  2. Correct way…

    const 
      getSlowPromise =_=> new Promise((resolve, reject) =>
        {
        setTimeout(() => { resolve('Slow promise fulfillment value'); }, 1000 );
        })
    , getFastPromise =_=> new Promise((resolve, reject) =>
        {
        setTimeout(() => { reject('Fast promise rejection value'); }, 200 );
        })
      ;
    const slowP = getSlowPromise; // no "()", to not call or exec anything
    const fastP = getFastPromise; // they are just a reference to
    
    slowP()
    .then( v =>
      {
      console.log(v);
      return fastP();  // need "()" to exec this other promise after this step
      })
    .then( 
        onFulfilled => console.log( 'fastP Fulfilled' )
      , onRejected  => console.log( onRejected )
      );
    Login or Signup to reply.
  3. 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

    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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search