Demystifying the background Scene of Async/Await in JavaScript

Rehmat Sayany
6 min readSep 16, 2023

--

Async Await background scene

JavaScript’s async/await syntax has revolutionized how developers handle asynchronous operations by making the code appear synchronous, thereby enhancing readability. This article delves deep into the mechanics of async/await with detailed examples.

The Essence of async

By prefixing a function with the async keyword, the function is marked to return a promise. This holds true irrespective of the actual return value of the function.

async function greet() {
return "Hello, World!";
}

console.log(greet()); // Outputs: Promise {<fulfilled>: "Hello, World!"}

Here, the function simply returns a string, but the async keyword ensures this string is wrapped inside a promise.

The Magic of await

The await keyword inside an async function pauses the function's execution until the promise it's placed before is resolved. In JavaScript, the non-blocking nature ensures that while one operation might be “waiting” due to an await, other operations aren't sitting idly. They're still progressing in the background. This behavior allows JavaScript to handle multiple asynchronous operations efficiently, even in its single-threaded environment.This makes your asynchronous code appear synchronous.

Let’s delve into some examples to clarify:

Example 1: Two 10-second Promises

console.log("Welcome to Coding Experience!");

let p1 = new Promise((resolve) => {
setTimeout(() => resolve('result1'), 10000);
});

let p2 = new Promise((resolve) => {
setTimeout(() => resolve('result2'), 10000);
});

async function demo() {
console.log("Start");
let startTime = Date.now();

let result1 = await p1;
console.log("P1 resolved in", Date.now() - startTime, "ms");

let result2 = await p2;
console.log("P2 resolved in", Date.now() - startTime, "ms");

console.log("End");
}

demo();

Output:

Welcome to Coding Experience!
Start
P1 resolved in 10000 ms
P2 resolved in 10001 ms
End

Confused ???? how does it resolve both promises in 10 seconds? I was confused too at first. Certainly! Let’s delve deeper into the inner workings of asynchronous operations in JavaScript and understand why the behavior in the above example occurs as it does.

Behind the Scenes: The JavaScript Event Loop

To understand why p2 in our example doesn't take another 10 seconds to resolve after p1, we need to understand the JavaScript Event Loop.

JavaScript is single-threaded, which means it can execute only one piece of code at a time. But how can it handle asynchronous operations like timeouts, promises, or event listeners? This is where the Event Loop comes into play.

The Event Loop constantly checks the message queue for pending messages (like a resolved promise) and processes them one by one. When we start an asynchronous operation, the browser or Node.js (depending on where you’re running your JavaScript) sets it up and then moves on, not waiting for it to complete.

Let’s revisit the first example:

When the JavaScript runtime encounters setTimeout in p1, it sets up a timer and then moves on, without waiting for the timer. Similarly, when it sets up the timer in p2, it doesn't wait for it either.

Effectively, both p1 and p2 are initiated almost simultaneously. Both of their timers start nearly at the same time. Thus, when the 10 seconds elapse, both promises are ready to be resolved.

When the demo function is called:

  1. “Start” is printed.
  2. The code encounters await p1 and waits for p1 to resolve. Meanwhile, the p2 timer is still running in the background.
  3. After 10 seconds, p1 resolves, and "P1 resolved" is printed.
  4. Immediately after, the code reaches await p2. However, since p2 was initiated almost simultaneously with p1, its 10-second timer has also elapsed. As such, it resolves nearly instantly.
  5. “P2 resolved” is printed without any noticeable delay after “P1 resolved”.
  6. “End” is printed.

Now one question arises in your mind how do p1 and p2 running "at the same time" in the background ??

It is due to JavaScript's asynchronous and non-blocking nature. Let's break this down:

JavaScript’s Single-Threaded Nature

JavaScript is single-threaded, meaning it processes one operation or task at a time in a single sequence (or thread) of events. At a glance, this might make it seem ill-suited for tasks that take some time to complete, like timeouts or network requests, because they would block the thread from doing anything else.

However, JavaScript overcomes this limitation through the use of an Event Loop and the Web API (in browsers) or equivalent mechanisms in other environments like Node.js.

How Does Asynchronous Execution Work?

When JavaScript encounters asynchronous operations, like setting a timeout or initiating a network request, it doesn’t execute them in the main thread. Instead:

  1. The JS runtime (like a browser) provides mechanisms (often termed the Web API) to handle these tasks outside of the main thread.
  2. When you initiate an asynchronous operation (like setTimeout in our example), it's handed off to this Web API.
  3. The main JavaScript thread continues executing subsequent code without waiting for the asynchronous operation to complete.
  4. Once the asynchronous operation is done (like when the timer expires for setTimeout), it puts a message (often with a callback function) in a message queue.
  5. The Event Loop consistently checks if the main JS thread is free. If it is, the Event Loop takes messages one by one from this queue and executes them.

Execution of p1 and p2

Given the above understanding, here’s what happens in our example:

  1. JavaScript encounters the setTimeout inside p1 and hands it off to the Web API, which starts a timer for 10 seconds.
  2. Almost immediately after, it encounters the setTimeout in p2 and also hands this to the Web API, initiating another timer for 10 seconds.
  3. Both these timers now run simultaneously in the background, outside the main JS thread.
  4. While the timers are running, JavaScript continues executing any code that comes after, which includes our async function.
  5. When the 10 seconds are up, both timers have completed. Their respective resolve functions (() => resolve('result1') and () => resolve('result2')) are placed in the message queue.
  6. Since we’re using await inside our async function, the resolution of p1 is handled first, printing "P1 resolved". Immediately after, because p2 had also completed in the background, its resolution is handled, printing "P2 resolved".

The key takeaway is that while JavaScript code execution in the main thread is single-threaded and sequential, operations in the background (like timeouts or network requests) can happen in parallel thanks to the browser’s Web API or Node.js’s mechanisms. This allows us to efficiently handle asynchronous operations in a non-blocking manner.

Let us take another example here to get more deeper understanding

Example 2: p1 resolve in 10-second and p2 resolve in 5 seconds

console.log("Welcome to Coding Experience!");
let p1 = new Promise((resolve, reject)=>{
setTimeout(()=> resolve('result'),10000);
})

let p2 = new Promise((resolve, reject)=>{
setTimeout(()=> resolve('result'),5000);
})

async function demo() {
console.log("Start");
let startTime = Date.now();

let result1 = await p1;
console.log("P1 resolved in", Date.now() - startTime, "ms");

let result2 = await p2;
console.log("P2 resolved in", Date.now() - startTime, "ms");

console.log("End");
}

demo();

Output:

Welcome to Coding Experience!
Start
P1 resolved in 10001 ms
P2 resolved in 10002 ms
End

What ??? I thought p2 would be printed first as it resolved in 5 seconds.

The behavior you’re seeing where p2 "waits" for p1 isn't due to the promises themselves or their internal resolution times. Both promises p1 and p2 are still resolving independently in the background, just like in our previous discussions.

The behavior you’re observing is due to the structure of the async function and the use of await:

In this async function:

  1. “Start” is printed.
  2. The execution encounters await p1, causing the function to pause and wait for p1 to resolve. Meanwhile, in the background, p2 also starts its timer because it was declared almost simultaneously with p1.
  3. After 5 seconds, p2 is resolved in the background. However, the function is still paused at await p1 and does not proceed until p1 is resolved.
  4. After 10 seconds, p1 resolves. "P1 resolved" is printed, and the function continues.
  5. Immediately after, the function reaches await p2. Since p2 has already resolved 5 seconds ago, there's no waiting. The "P2 resolved" line is printed almost immediately after the "P1 resolved" line.
  6. “End” is printed.

The sequential waiting is due to the sequential use of await in the function. The await keyword makes the function wait for the promise to resolve before moving on. Even though p2 has resolved while the function was waiting for p1, the function does not "know" or "react" to this until it reaches the line await p2. At that point, since p2 is already resolved, it continues without any further delay.

Conclusion

Hope I did justice and cleared your doubts. In JavaScript, the non-blocking nature ensures that while one operation might be “waiting” due to an await, other operations aren't sitting idly. They're still progressing in the background. This behavior allows JavaScript to handle multiple asynchronous operations efficiently, even in its single-threaded environment.

It’s this understanding of the inner workings of the Event Loop and asynchronous operations that allows developers to write efficient, non-blocking code using async/await, making the most of JavaScript’s capabilities.

--

--

Rehmat Sayany

Full Stack developer @westwing passionate about NodeJS, TypeScript, React JS and AWS.