Search code examples
javascriptes6-promisejobsasynccallback

Why promise state shows pending, when it has resolved to another promise


Below code snippet I have taken from the famous YDKJS - Async & Performance series, and have made a few changes for the sake of understanding.

var p3 = new Promise( function(resolve,reject){
    resolve( "B" );
} );
var p1 = new Promise( function(res,rej){
  resolve( p3 ); // equivalent to p3.then(res)
} );
var p2 = new Promise( function(resolve,reject){
    resolve( "A" );
} );
p1.then( function(v){
    console.log("In p1");
    console.log(p1);
} );
p2.then( function(v){
    console.log("In p2");
    console.log(p1);
} ); 

Although the order in which the results are logged are as per desire, but the state of promise p1 shows pending (at console.log(p1)) which I could not understand. I have tried to depict through pictures (at the end) what I have understood so far, so you can correct me where I'm wrong.

Result

enter image description here

At Stage 1: p1 resolves to an already resolved promise, p3. I know p1 would still remain pending and would not resolve to a value yet. But I have heard that resolve(p3) is equivalent to

p3.then(res);  // res is a resolve callback of p1

So based on this assumption, since p3 is both resolved and has a registered handler (callback res of p1) it will be inserted into Micro Task Queue (MTQ).

At Stage 2: p2 is both resolved and has a registered handler (in purple) it will be appended to MTQ.

At Stage 3: Now as there's left nothing on stack to execute, the cb (in yellow) which was first inserted in MTQ will get on stack for execution. Here are my Queries: When res('B') executes will it mark the p1's state as Fulfilled? Since res is the callback associated with p1.

As p1 already has a registered handler (when p1.then was called) besides being fulfilled, wouldn't it append to the MTQ as depicted at Stage 4?

If so, then why is p1 still showing as pending even at Stage 5?

And at Stage 6: How does p1 gets fulfilled all of a sudden?

Please assist me to understand where I'm wrong in below pictorial depiction?

enter image description here


Solution

  • Why promise state shows pending, when it has resolved to another promise?

    That's just terminology. The implementation does not distinguish between the "unresolved" and "resolved" pending states.

    When res('B') executes will it mark the p1's state as Fulfilled?

    Yes.

    Wouldn't the callback registered on p1 be appended to the MTQ as depicted at Stage 4? Why is p1 still showing as pending even at Stage 5?

    Your reasoning is mostly correct. Yes, it would. In fact, if you had written p3.then(resolve) in your p1 execution callback, that's exactly what would have happened and the Stage 5 console.log would have printed Promise {<fulfilled>: 'B'}.

    I have heard that resolve(p3) is equivalent to p3.then(resolve);. So based on this assumption…

    That assumption is not exactly valid. It is indeed mostly equivalent in behaviour, but it is not equivalent in microtask timing. When you pass a thenable (a promise) to resolve, it will actually schedule the call to the .then(resolve, reject) in a new microtask instead of synchronously calling it from within resolve().

      • a: p3 is created, p3 is fulfilled with "B"
      • b: p1 is created, resolve(p3) accesses p3.then, finds it to be a method, and schedules a callback to invoke it
      • c: p2 is created, p2 is fulfilled with "A"
      • d: p1.then(…) registers a callback on the promise
      • e: p2.then(…) sees that p2 is already fulfilled and schedules the callback
    1. The task from step 1b runs and calls p3.then(resolve, reject) to adopt its state to p1. It sees that p3 is already fulfilled and schedules the callback
    2. The task from step 1e runs:
      • a: it logs "In p2"
      • b: it logs p1, which is still pending (but already resolved to p3)
    3. The task from step 2 runs and calls resolve("B"). This fulfills p1 and schedules the callback that was registered on it.
    4. The task from step 4 runs:
      • a: it logs "In p1"
      • b: it logs p1, which had been fulfilled with "B" in step 4.

    You could read the spec for such intricate details, but really you just shouldn't rely on them (and they are changing from time to time - in fact the very detail that you missed is proposed to change). You have two independent promise chains, and you should not make any assumptions on the order of callbacks between p1 and p2. If it was important, you'd make the promises dependent on each other.