To answer your questions, let me first try to explain how the code in your question executes.
First promise constructor is executed
new Promise((resolve) => {
console.log(1);
resolve();
})
logging 1 on the console and resolves the promise synchronously
As the promise is resolved, a job is enqueued in the micro-task queue to execute the fulfilment handler passed to the then() method
new Promise((resolve) => {
...
})
.then(async () => {
console.log(2);
})
micro-task queue: [ job(console.log(2)) ]
console output: 1
Second promise constructor is executed
new Promise((resolve) => {
console.log("a");
resolve();
})
logging 'a' on the console and resolves the promise synchronously
As the promise is resolved, a job is enqueued in the micro-task queue to execute the fulfilment handler passed to the then() method
new Promise((resolve) => {
...
})
.then(async () => {
console.log("b");
})
micro-task queue: [ job(console.log(2)), job(console.log('b')) ]
console output: 1 a
After the synchronous execution of the script ends, javascript starts processing the micro-task queue
micro-task queue: [ job(console.log(2)), job(console.log('b')) ]
console output: 1 a
First job from the micro-task is dequeued and processed, logging 2 on the console
micro-task queue: [ job(console.log('b')) ]
console output: 1 a 2
As the fulfilment handler passed to the first then() method of the first promise is an async function, fulfilment handler implicitly returns a promise.
If the return value of the callback function passed to the then method is also a promise, the promise returned by the then() method is resolved to the promise returned by its callback function.
In other words, fate of the outer promise (returned by the then() method) depends on the inner promise (returned by its callback function). If the inner promise fulfils, outer promise will also fulfil with the same value.
Following code example demonstrates how the promise returned by the then method is resolved to the promise returned by its callback function:
const outerPromise = new Promise((resolveOuter, rejectOuter) => {
const innerPromise = new Promise((resolveInner, rejectInner) => {
resolveInner();
});
// resolve/reject functions of the outer promise are
// passed to the `then()` method of the inner promise
// as the fulfilment/rejection handlers respectively
innerPromise.then(resolveOuter, rejectOuter);
});
To resolve the outer promise to the inner promise, another job will be enqueued in the micro-task queue
micro-task queue: [ job(console.log('b')), job(resolve(outerPr, innerPr) ]
console output: 1 a 2
Next job in the micro-task queue is dequeued and processed, logging 'b' on the console
micro-task queue: [ job(resolve(outerProm, innerProm) ]
console output: 1 a 2 b
As the callback that logged 'b' on the console implicitly returned undefined, promise returned by the wrapper then() method is fulfilled with the value of undefined.
As a result, another job is enqueued in the micro-task queue to execute the fulfilment handler of the next then() method
new Promise((resolve) => {
...
})
.then(() => {
...
})
.then(() => {
console.log("c");
})
micro-task queue: [job(resolve(outerProm, innerProm), job(console.log('c')]
console output: 1 a 2 b
Next job in the micro-task queue is dequeued and processed. As the next job is related to one promise waiting for another promise to settle (resolve or reject), the inner promise is resolved with the value of undefined (value returned by the corresponding callback function) (See step 7).
As a result, another job in enqueued in the micro-task queue to execute the fulfilment handler of the inner promise. Fulfilment handler in this case is the resolve function of the outer promise. Calling it will resolve the outer promise
micro-task queue: [ job(console.log('c'), job(resolve(outerProm), ]
console output: 1 a 2 b
Next job in the micro-task queue is dequeued and processed, logging 'c' on the console
micro-task queue: [ job(resolve(outerProm), ]
console output: 1 a 2 b c
As the callback that logged 'c' on the console implicitly returned undefined, promise returned by the wrapper then() method is fulfilled with the value of undefined.
As a result, another job is enqueued in the micro-task queue to execute the fulfilment handler of the next then() method
new Promise((resolve) => {
...
})
.then(() => {
...
})
.then(() => {
...
})
.then(() => {
console.log("d");
})
micro-task queue: [ job(resolve(outerProm), job(console.log('d') ]
console output: 1 a 2 b c
Next job in the micro-task queue is dequeued and processed, resolving the promise returned by the then() method of the first promise
new Promise((resolve) => {
...
})
.then(async () => {
console.log(2);
})
As a result, a job is enqueued in the micro-task queue to execute its fulfilment handler
micro-task queue: [ job(console.log('d'), job(console.log(3) ]
console output: 1 a 2 b c
Next job in the micro-task queue is dequeued and processed, logging 'd' on the console
micro-task queue: [ job(console.log(3) ]
console output: 1 a 2 b c d
After the execution of callback that logged 'd' on the console, another job is enqueued in the micro-task queue to log 'e' on the console
micro-task queue: [ job(console.log(3), job(console.log('e') ]
console output: 1 a 2 b c d
Finally the last two job will be dequeued and processed one after the other and the final output will be:
micro-task queue: [ ]
console output: 1 a 2 b c d 3 e
See the detailed explanation above.
If you understand the explanation above, you now also understand the output in the codepen.
Yes. As async function implicitly returns a promise, returning Promise.resolve() from the callback function of a then() method will have the same effect (see step 7 above).
asyncwithout a correspondingawait?1, 2, 3and the other sequence isa, b, c, d, e, and they are arbitrarily interleaved. It doesn't matter, and is in fact subject to change.