javascriptasynchronouspromisees6-promise

Why does `promise.finally` execute after its caller's `then`?


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.


Solution

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