2

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?

Fedy2
  • 3,147
  • 4
  • 26
  • 44
  • 4
    your mixing promises and async, don't do that. Pick one or the other – Liam Mar 23 '22 at 16:26
  • 4
    Your also using the [explicit constructor promise anti-pattern](https://stackoverflow.com/questions/23803743/what-is-the-explicit-promise-construction-antipattern-and-how-do-i-avoid-it). If you get rid of these two things your code will be much easier to understand – Liam Mar 23 '22 at 16:28
  • It's really not clear what your asking. Why is it doing that? Because that's how promises work? – Liam Mar 23 '22 at 16:29
  • @Liam is right. Basically, I think the async-with-no-await behaves like a fire-and-forget, so should be like starting two threads (if it was a multithread framework) in parallel. – Mario Vernari Mar 23 '22 at 16:30
  • Thank you all for the comments. I know that the code of the cases is wrong/incorrect/unclear, but I am curious to know what is happening with this dirty code :) – Fedy2 Mar 23 '22 at 16:30
  • Well, not really. JS is (mostly) single threaded. – Liam Mar 23 '22 at 16:30
  • 1
    What the OP is trying to ask is why does a `.catch` block affects the order of execution. – yqlim Mar 23 '22 at 16:31
  • 2
    @Liam _"It's really not clear what your asking. Why is it doing that?"_ - The only difference between example 1 and 2 is the use of `Promise.resolve()` in the `async` function. So why is the output different two the one from example 1? – Andreas Mar 23 '22 at 16:32
  • You have to get rid of the `async` in the second and third case – Nullndr Mar 23 '22 at 16:40
  • @Nullable And why? This won't change anything as this only indicates that the return value of that function is a `Promise`. And that's the case in all three examples. Only difference is ex 1 with an implicit `Promise` – Andreas Mar 23 '22 at 17:00
  • The high level explanation is when you put more promises in the chain or promises that resolve differently, it can influence the relative timing vs. other things (more or differently timed trips through the event loop). As others have said, this code is a total mess and it is not worth diagnosing in detail because it's not a real world situation at all with no real asynchronous operations and a bunch of anti-pattern code structures. Sequencing of real world code is driven by the timing of actual asynchronous operations completing, not artificial do-nothing promises. – jfriend00 Mar 23 '22 at 17:54

1 Answers1

0

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
Fedy2
  • 3,147
  • 4
  • 26
  • 44