Javascript Async/Await/Microtask Queue Explained

This article is designed to clarify some confusion around async/await usage and how it works compared to a regular Promise.

Ah the much loved call back hell. Since JS8 or ECMAScript 2017 (ECMAScript is just a standard set by ECMA organization), using promises became a lot more practical due to these two magic keywords async and await. I won’t go into detail on callbacks as it is its own topic.

The async keyword can be used only with a function declaration. It tells the JavaScript runtime environment (V8, Node.js, or Deno) that it should wrap the function body in a Promise. So it returns a promise which resolves to a value or throws an exception.

The await keyword may only be used within a Promise object. It informs the JavaScript runtime environment that it should hold up program execution until the associated Promise resolves. Another constraint is that await can be used only inside an async function.

Using the async/await combo we can write javascript in a sequential order, making it a lot easier to read and debug. Let us looks at this example to illustrate the differences between using async/await vs .then notation

const multiply = (num1, num2) =>
new Promise((resolve) => resolve(num1 * num2))
const sum = (total, num3) =>
new Promise((resolve) => resolve(total + num3))
const multiplyThenSumAsync = async (num1, num2, num3) => {
const product = await multiply(num1, num2)
const sumTotal = await sum(product, num3)
return sumTotal
}
const getResult = () => (
multiply(10, 20)
.then(product => sum(product, 30))
.then(total => {
console.log('total using regular promise', total)
return total
})
)
const getResultAsync = async () => {
const product = await multiply(10, 20)
const total = await sum(product, 30)
console.log('total using async', total)
}
// Just for verification, We still love you callbacks :)
getResult().then(res =>
getResultAsync().then(resAsync =>
console.log('Same result => ', resAsync === res)
)
)

So, same result but different ways of extracting it. But there is a key difference in play here. First lets understand these three terms:

Tasks: are scheduled so the browser can get from its internals into JavaScript/DOM land and ensures these actions happen sequentially. Between tasks, the browser may render updates.

Microtasks: are usually scheduled for things that should happen after the function or program which created it exits and only if the JavaScript execution stack is empty, but before returning control to the event loop.

Microtask Queue: is a virtually managed data structure where microtasks get added and executed. This is processed after callbacks/scripts/timeouts as long as no other JavaScript is mid-execution, and at the end of each task.

Different browsers have slightly different implementation of the microtask queue but mozilla has good documentation to get a deeper dive.

Back to our example. In getResult the callback returned from the Promise, gets assigned its own thread and added to the microtask queue. A property of the microtask queue is that it does not get executed until nothing else is running (i.e: the call stack is empty).

In getResultAsync microtask queue is also involved. Using the async keyword the execution of the function body gets paused, and the rest of the async function gets run in a microtask. So we return the execution control to the global scope so that javascript engine can continue to do other jobs in the call stack (events, other scripts, etc.), and once the stack is empty we resume with the function body.

React enthusiast, AVID learner