skip to Main Content

Consider this:

const a = () => {
  return new Promise((resolve) => {
    resolve(1);
  });
};

const b = () => {
  return new Promise((resolve) => {
    a()
      .then((aR) => {
        console.log(aR);
        resolve(2);
      })
      .finally(() => {
        console.log("A");
      });
  });
};

const c = () => {
  b().then((bR) => {
    console.log(bR);
  });
};

c();

Why it prints 1 2 A, not 1 A 2? If rewriting as synchronous logic, it should look like this:

const a = () => 1

const b = () => {
  try {
    const aR = a()
    console.log(aR)
    return 2
  } finally {
   console.log("A")
  }
};

const c = () => {
  try {
  const bR = b()
  console.log(bR)
  } catch {}
};

c();

It prints 1 A 2


The problem is originally discovered by @rebuilding127 on Segmentfault, a StackOverflow-like Chinese forum. I think it’s worth translating & publishing here.

2

Answers


  1. Chosen as BEST ANSWER

    You can see this chain-call:

        a()
          .then((aR) => {
            console.log(aR);
            resolve(2);
          })
          .finally(() => {
            console.log("A");
          });
    

    This is a chained invocation, where a().then(...) returns a Promise, it will output A after the returned Promise is fulfilled, not after a() is fulfilled. So resolve(2) will enqueue b().then before fulfilling a().then and enqueuing a().finally.

    To make it clearer, I rewrited the code:

    const f = () => {
      const a = new Promise((resolve) => {
        const cb1 = (result1) => {
          console.log(result1);
          resolve(2);
        };
        const cbA = () => console.log('A');
        const b = Promise.resolve(1);
        const c = b.then(cb1);
        c.finally(cbA);
      });
      return a;
    };
    
    const cb2 = (result2) => console.log(result2);
    f().then(cb2);

    The execution unfolds like this:

    1. Start executing f
    2. Start executing a
    3. b is immediately fulfilled, causing cb1 to be enqueued into the microtask queue (now queue = [cb1])
    4. c's executor enqueues
    5. f returns
    6. Start executing cb1 from the queue (now queue = [])
    7. Outputs 1, resolves a, causing cb2 to be enqueued (now queue = [cb2])
    8. cb1 returns, causing c to be resolved, then cbA to be enqueued (now queue = [cb2, cbA])
    9. Start executing cb2 from the queue (now queue = [cbA])
    10. Outputs 2
    11. Start executing cbA from the queue; now queue = [].
    12. Outputs A, tick ends

    Also published under the original problem on SegmentFault.


  2. To agree with typed-sigterm‘s answer, this is because resolve(2) will instantly resolve the new Promise(...) returned by b(). That "instantly" doesn’t mean synchronously, but rather that nothing is ensuring that the Promise will wait for the finally block.

    Unlike the finally block in a try/catch statement, Promise.finally can be thought of as a then(x, y) call with the same handler function for x and y (but with the returned and thrown handler result discarded). It does not happen before the call to resolve().

    If you do want this kind of behavior, where some possibly-asynchronous behavior happens before the call to resolve, you can return a Promise from then or call resolve within new Promise with a Promise. These don’t even have to be real Promises, they just have to have to be "Promise-like" or "thenable" by having a callable then method.

    Of course, at that point you can rewrite to avoid the new Promise entirely:

    const a = () => Promise.resolve(1);
    
    const b = () => {
      return a()                // no new Promise():
        .then((aR) => {         // run a(),
          console.log(aR);      // log,
          return 2;             // return 2,
        })
        .finally(() => {        // but this runs before
          console.log("A");     // the b() Promise settles
        });                     // because b() returns the
    };                          // Promise that finally() returns
    
    const c = () => {
      return b().then((bR) => { // return the promise, which is
        console.log(bR);        // irrelevant but good practice
      });
    };
    
    c();
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search