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");
});
- execution traverse the Promise constructors for Promise A, B and C
- C is resolved immediately
- the execution of callback
then1
is added in the microtask queue
- execution continues chaining promises until
resolve(b)
b
is a Promise that is passed as resolve value for Promise A. This generates a new microtask that chains B to A.
- End of main block, two microtasks in the queue:
then1
, chain-B-to-A
.
- execution of
then1
:
- prints in console "THEN"
- resolves Promise
b
- the Promise generated by
then
resolves and enqueues a microtask for catch1
- execution of
chain-B-to-A
:
- in pseudo code:
b.then(a.resolve, a.reject)
- the Promise B is already resolved therefore the microtask
then-B-A
is enqueued
- microtask queue a this moment:
catch
, then-B-A
- execution of
catch1
:
- nothing to do with the callback
- the Promise generated by catch resolves and enqueues a microtask for
finally1
- execution of
then-B-A
:
- Promise A resolves and enqueues a microtask for
externalThen
- microtask queue a this moment:
finally1
, externalThen
- execution of
finally1
: prints "FINALLY"
- execution of
externalThen
: prints "DONE"
- 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");
});
execution traverse the Promise constructors for Promise A, B and C
Promise C is resolved with Promise D, this generates a microtask to chain D to C
execution continues chaining promises until resolve(b)
b
is a Promise that is passed as resolve value for Promise A. This generates a new microtask that chains B to A.
End of main block, two microtasks in the queue: chain-D-to-C
, chain-B-to-A
.
execution of chain-D-to-C
:
- in pseudo code:
d.then(c.resolve, c.reject)
- the microtask
then1
is enqueued
execution of chain-B-to-A
:
- in pseudo code:
b.then(a.resolve, a.reject)
- Promise B is not resolved, nothing else happens
microtask queue a this moment: then1
execution of then1
:
- prints in console "THEN"
- resolves Promise
b
, this enqueues the then-B-A
microtask
- the Promise generated by
then
resolves and enqueues a microtask for catch1
microtask queue a this moment: then-B-A
, catch1
execution of then-B-A
:
- Promise A resolves and enqueues a microtask for
externalThen
execution of catch1
:
- nothing to do with the callback
- the Promise generated by catch resolves and enqueues a microtask for
finally1
microtask queue a this moment: externalThen
, finally1
execution of externalThen
: prints "DONE"
execution of finally1
: prints "FINALLY"
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:
- microtask queue a this moment:
then1
- execution of
then1
:
- prints in console "THEN"
- resolves Promise
b
, this enqueues the then-B-A
microtask
- the Promise generated by
then
resolves and enqueues a microtask for finally1
- microtask queue a this moment:
then-B-A
, finally1
- execution of
then-B-A
:
- Promise A resolves and enqueues a microtask for
externalThen
- execution of
finally1
: prints "FINALLY"
- microtask queue a this moment:
externalThen
- execution of
externalThen
: prints "DONE"
- no more microtaks, code terminates