What is happening with this chain of Promises?

Viewed 101

I am scratching my head trying to understand what is happening with the following cases.

Case 1:

async function doSomething(): Promise<void> {
    return new Promise(resolve => {

        const promise = (async () => "A")();

        promise
            .then(() => {
                console.log("THEN");
                resolve();
            })
            .catch(() => {
                console.log("CATCH");
            })
            .finally(() => {
                console.log("FINALLY");
            });
    });
}

async function main() {
    await doSomething();
    console.log("DONE");
}

main().catch(console.error);

Output, as expected:

THEN
FINALLY
DONE

But in case 2:

async function doSomething(): Promise<void> {
    return new Promise(resolve => {

        const promise = (async () => Promise.resolve("A"))();

        promise
            .then(() => {
                console.log("THEN");
                resolve();
            })
            .catch(() => {
                console.log("CATCH");
            })
            .finally(() => {
                console.log("FINALLY");
            });
    });
}

async function main() {
    await doSomething();
    console.log("DONE");
}

main().catch(console.error);

The output is:

THEN
DONE
FINALLY

And case 3:

async function doSomething(): Promise<void> {
    return new Promise(resolve => {

        const promise = (async () => Promise.resolve("A"))();

        promise
            .then(() => {
                console.log("THEN");
                resolve();
            })
            .finally(() => {
                console.log("FINALLY");
            });
    });
}

async function main() {
    await doSomething();
    console.log("DONE");
}

main().catch(console.error);

The output is:

THEN
FINALLY
DONE

What is happening exactly to the chain of promises?

Why the case 2 doesn't print "THEN, FINALLY, DONE"?

Why the case 3 behaves differently from case 2?

1 Answers

I might have understood what happened with the 3 cases.

TLDR

The difference in number of promises and the resolution of promises with promise changes the sequence of microtask enqueued causing the difference in print statements.

Code simplification

First of all lets try to simplify the code removing the async/await. I will use the code of case 1 for the simplification.

The main block

async function doSomething(): Promise<void> {
    return new Promise(resolve => {
        ...
    });
}
async function main() {
    await doSomething();
    console.log("DONE");
}

main().catch(console.error);

can be rewrote to:

function doSomething(): Promise<void> {
   // Promise A
   return new Promise(resolve => {
     // Promise B
     const b = new Promise(resolve => {
            ...
     });
     resolve(b);
   });
}

doSomething()
 .then(function externalThen() {
   console.log("DONE");
 });

The promise declaration for case 1:

const promise = (async () => "A")();

becomes:

const promise = Promise.resolve("A");

and for case 2 and 3:

const promise = (async () => Promise.resolve("A"))();

becomes:

const promise = new Promise(resolve => {
         resolve(Promise.resolve("A"))
});

Putting all together and removing unnecessary functions and variable declarations:

// Promise A
new Promise(resolve => {
    // Promise B
    const b = new Promise<void>(resolve => {
        // Promise C
        new Promise(resolve => {
            resolve("A");
        })
        .then(function then1() {
            console.log("THEN");
            resolve();
        })
        .catch(function catch1() {
            console.log("CATCH");
        })
        .finally(function finally1() {
            console.log("FINALLY");
        });
    });
    resolve(b);
})
.then(function externalThen() {
    console.log("DONE");
});

For case 2 and 3 the Promise C declaration becomes:

// Promise C
new Promise(resolve => {
    // Promise D
    resolve(Promise.resolve("A"));
})

How Promise code is executed

  • the executor function passed to the Promise constructor is executed immediately.
  • the callbacks passed to then, catch and finally methods are executed as microtask when the promise fulffils.
  • microtask are enqueue in a microtask queue
  • the microtask queue is emptied and all the tasks executed, in FIFO order, when the JS stack is empty
  • passing a Promise p1 to the resolve callback of a Promise p2 generates a new microtask that chains p1 to p2. The then method of p1 is called passing the resolve and reject callbacks of p2.

Execution case 1

// Promise A
new Promise(resolve => {
    // Promise B
    const b = new Promise<void>(resolve => {
        // Promise C
        new Promise(resolve => {
            resolve("A");
        })
        .then(function then1() {
            console.log("THEN");
            resolve();
        })
        .catch(function catch1() {
            console.log("CATCH");
        })
        .finally(function finally1() {
            console.log("FINALLY");
        });
    });
    resolve(b);
})
.then(function externalThen() {
    console.log("DONE");
});
  1. execution traverse the Promise constructors for Promise A, B and C
  2. C is resolved immediately
  3. the execution of callback then1 is added in the microtask queue
  4. execution continues chaining promises until resolve(b)
  5. b is a Promise that is passed as resolve value for Promise A. This generates a new microtask that chains B to A.
  6. End of main block, two microtasks in the queue: then1, chain-B-to-A.
  7. execution of then1:
    1. prints in console "THEN"
    2. resolves Promise b
    3. the Promise generated by then resolves and enqueues a microtask for catch1
  8. execution of chain-B-to-A:
    1. in pseudo code: b.then(a.resolve, a.reject)
    2. the Promise B is already resolved therefore the microtask then-B-A is enqueued
  9. microtask queue a this moment: catch, then-B-A
  10. execution of catch1:
    1. nothing to do with the callback
    2. the Promise generated by catch resolves and enqueues a microtask for finally1
  11. execution of then-B-A:
    1. Promise A resolves and enqueues a microtask for externalThen
  12. microtask queue a this moment: finally1, externalThen
  13. execution of finally1: prints "FINALLY"
  14. execution of externalThen: prints "DONE"
  15. no more microtaks, code terminates

Execution case 2

// Promise A
new Promise(resolve => {
    // Promise B
    const b = new Promise<void>(resolve => {
        // Promise C
        new Promise(resolve => {
           // Promise D
           resolve(Promise.resolve("A"));
        })
        .then(function then1() {
            console.log("THEN");
            resolve();
        })
        .catch(function catch1() {
            console.log("CATCH");
        })
        .finally(function finally1() {
            console.log("FINALLY");
        });
    });
    resolve(b);
})
.then(function externalThen() {
    console.log("DONE");
});
  1. execution traverse the Promise constructors for Promise A, B and C

  2. Promise C is resolved with Promise D, this generates a microtask to chain D to C

  3. execution continues chaining promises until resolve(b)

  4. b is a Promise that is passed as resolve value for Promise A. This generates a new microtask that chains B to A.

  5. End of main block, two microtasks in the queue: chain-D-to-C, chain-B-to-A.

  6. execution of chain-D-to-C:

    1. in pseudo code: d.then(c.resolve, c.reject)
    2. the microtask then1 is enqueued
  7. execution of chain-B-to-A:

    1. in pseudo code: b.then(a.resolve, a.reject)
    2. Promise B is not resolved, nothing else happens
  8. microtask queue a this moment: then1

  9. execution of then1:

    1. prints in console "THEN"
    2. resolves Promise b, this enqueues the then-B-A microtask
    3. the Promise generated by then resolves and enqueues a microtask for catch1
  10. microtask queue a this moment: then-B-A, catch1

  11. execution of then-B-A:

    1. Promise A resolves and enqueues a microtask for externalThen
  12. execution of catch1:

    1. nothing to do with the callback
    2. the Promise generated by catch resolves and enqueues a microtask for finally1
  13. microtask queue a this moment: externalThen, finally1

  14. execution of externalThen: prints "DONE"

  15. execution of finally1: prints "FINALLY"

  16. no more microtaks, code terminates

Execution case 3

// Promise A
new Promise(resolve => {
    // Promise B
    const b = new Promise<void>(resolve => {
        // Promise C
        new Promise(resolve => {
           // Promise D
           resolve(Promise.resolve("A"));
        })
        .then(function then1() {
            console.log("THEN");
            resolve();
        })
        .finally(function finally1() {
            console.log("FINALLY");
        });
    });
    resolve(b);
})
.then(function externalThen() {
    console.log("DONE");
});

All identical to case 2 until here:

  1. microtask queue a this moment: then1
  2. execution of then1:
    1. prints in console "THEN"
    2. resolves Promise b, this enqueues the then-B-A microtask
    3. the Promise generated by then resolves and enqueues a microtask for finally1
  3. microtask queue a this moment: then-B-A, finally1
  4. execution of then-B-A:
    1. Promise A resolves and enqueues a microtask for externalThen
  5. execution of finally1: prints "FINALLY"
  6. microtask queue a this moment: externalThen
  7. execution of externalThen: prints "DONE"
  8. no more microtaks, code terminates
Related