Demystifying the background Scene of Async/Await in JavaScript
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 anasync
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:
- “Start” is printed.
- The code encounters
await p1
and waits forp1
to resolve. Meanwhile, thep2
timer is still running in the background. - After 10 seconds,
p1
resolves, and "P1 resolved" is printed. - Immediately after, the code reaches
await p2
. However, sincep2
was initiated almost simultaneously withp1
, its 10-second timer has also elapsed. As such, it resolves nearly instantly. - “P2 resolved” is printed without any noticeable delay after “P1 resolved”.
- “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:
- The JS runtime (like a browser) provides mechanisms (often termed the Web API) to handle these tasks outside of the main thread.
- When you initiate an asynchronous operation (like
setTimeout
in our example), it's handed off to this Web API. - The main JavaScript thread continues executing subsequent code without waiting for the asynchronous operation to complete.
- 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. - 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:
- JavaScript encounters the
setTimeout
insidep1
and hands it off to the Web API, which starts a timer for 10 seconds. - Almost immediately after, it encounters the
setTimeout
inp2
and also hands this to the Web API, initiating another timer for 10 seconds. - Both these timers now run simultaneously in the background, outside the main JS thread.
- While the timers are running, JavaScript continues executing any code that comes after, which includes our async function.
- 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. - Since we’re using
await
inside our async function, the resolution ofp1
is handled first, printing "P1 resolved". Immediately after, becausep2
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:
- “Start” is printed.
- The execution encounters
await p1
, causing the function to pause and wait forp1
to resolve. Meanwhile, in the background,p2
also starts its timer because it was declared almost simultaneously withp1
. - After 5 seconds,
p2
is resolved in the background. However, the function is still paused atawait p1
and does not proceed untilp1
is resolved. - After 10 seconds,
p1
resolves. "P1 resolved" is printed, and the function continues. - Immediately after, the function reaches
await p2
. Sincep2
has already resolved 5 seconds ago, there's no waiting. The "P2 resolved" line is printed almost immediately after the "P1 resolved" line. - “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.