Skip to main content

Command Palette

Search for a command to run...

28. ASYNC | Asynchronous Runtime Model in JavaScript

This article will explains how JavaScript handles asynchronous operations despite being single-threaded.

Updated
β€’8 min read
S
Full-Stack Developer with 4 years' experience, specializing in backend development. Skilled in JavaScript, React, Python, Databases, and AWS. Known for building scalable web apps, leading teams, and maintaining strong client communication. Upskilling in Generative AI.

Is JavaScript single threaded or multi-threaded?

JavaScript is single-threaded by nature, meaning it has one main thread and processes one instruction at a time. It cannot process multiple instructions simultaneously in the same thread.

However, modern JavaScript environments (browsers and Node.js) provide mechanisms like,

  • Event Loop

  • Callback Queue (Macro task Queue)

  • High Priority Queue (Microtask Queue)

  • Web APIs (in browsers) / Node APIs (in Node.js)

that allow it to handle multiple tasks concurrently without blocking the main thread, giving an appearance of multi-threading.


Being single threaded, how JavaScript achieves non-blocking behavior?

JavaScript execution relies on several components:

Call Stack:

  • JavaScript executes code using Call Stack, which tracks the currently executing function.

  • Main function (the entire script) is automatically added to the call stack.

  • Functions are pushed onto the stack when called and popped off when they return.

  • In Call Stack, synchronous code is executed.

Web APIs (or background tasks):

  • When an asynchronous operation (like a setTimeout, a network request, a DOM event or I/O operation) is encountered in the call stack, it is handed off to the browser's or Node.js's built-in Web APIs to run in the background.

Callback Queue (Macro task Queue):

  • Once the asynchronous operation in the Web API is complete, its associated callback function is placed into the callback queue (a First-In, First-Out or FIFO structure).

High Priority Queue (Microtask Queue):

  • Microtasks are usually created by promises.

  • When a promise is resolved or rejected, its .then() or .catch() callbacks are added to the Microtask Queue.

  • Microtasks have higher priority than Macrotasks (like setTimeout callbacks).

Event Loop:

  • This continuous process constantly monitors two things:

    1. Whether the call stack is empty.

    2. Whether there are any pending callbacks in the microtask queue or callback queue.

  • When the Call Stack is empty, the Event Loop first processes callbacks from the Microtask Queue by pushing them onto the Call Stack for execution, and then processes callbacks from the Callback Queue.

πŸ’‘
To understand this, please visit Loupe.

Synchronous Code Examples

console.log("Hi-1");

function hello() {
    console.log("Inside hello function!");
}

for (let i=0; i<=5; i++){
    console.log(i);
}

hello();

console.log("Hi-2");

/* Output:
 Hi-1
 0
 1
 2
 3
 4
 5
 Inside hello function!
 Hi-2
**/
function listLocations(locations){
    locations.forEach((location) => {
        console.log(location);
    });
}

const myLocation = ['Delhi', 'Punjab'];

listLocations(myLocation);

/* Output:
 Delhi
 Punjab
**/

Asynchronous Code Examples

console.log('Starting Up!');

setTimeout(function two() {
    console.log('Two Seconds!');
}, 2000);

setTimeout(function zero() {
    console.log('Zero Seconds!');
}, 0);

console.log('Finishing Up!');

/* Output:
 Starting Up!
 Finishing Up!
 Zero Seconds!
 Two Seconds!
**/


/* Control Flow:
 1. The entire script is loaded, and the global execution context is pushed onto the Call Stack.
 2. Line 1: console.log is pushed onto the Call Stack, executed immediately, and then removed.
 3. Line 3: setTimeout is pushed onto the Call Stack. The JavaScript engine removes it from the stack and registers it with the Web API, where a timer of 2 seconds begins.
 4. Line 7: Another setTimeout is pushed onto the Call Stack. It is then handed over to the Web API, where a timer of 0 seconds starts.
 5. Line 11: console.log is pushed onto the Call Stack, executed, and then removed.
 6. Once the 0-second timer completes, its callback function is added to the Callback Queue.
 7. After 2 seconds, the second setTimeout callback is also placed into the Callback Queue.
 8. The Event Loop continuously checks whether the Call Stack is empty. When it is, the Event Loop moves callbacks from the Callback Queue to the Call Stack, one at a time, for execution.
**/
console.log("Hi-1");

function hello() {
    console.log("Inside hello function!");
}

function hi() {
    console.log("Inside hi function!");
}

setTimeout(hello, 0);

setTimeout(hi, 5000);

for (let i=0; i<=5; i++){
    console.log(i);
}

console.log("Hi-2");

/* Output:
 Hi-1
 0
 1
 2
 3
 4
 5
 Hi-2
 Inside hello function!
 Inside hi function!
**/


/* Control Flow:
 1. The entire script is loaded and executed, and the global execution context is pushed onto the Call Stack.
 2. Line 1: console.log is pushed onto the Call Stack, executed, and then removed from the stack.
 3. Line 3: The function declaration is registered in memory but not executed, since it is not invoked.
 4. Line 7: Similarly, this function declaration is stored but not executed because it is not called.
 5. Line 11: setTimeout is pushed onto the Call Stack. The JavaScript engine then removes it from the stack and hands it over to the Web API, where a timer of 0 seconds starts.
 6. Line 13: Another setTimeout is pushed onto the Call Stack. It is then transferred to the Web API, where a timer of 5 seconds begins.
 7. Line 15 & 16: The for loop is pushed onto the Call Stack and executed synchronously.
 8. Line 19: console.log is pushed onto the Call Stack, executed, and then removed.
 9. Once the 0-second timer completes, its callback function is placed into the Callback Queue.
 10. After 5 seconds, the second setTimeout callback is also added to the Callback Queue.
 11. The Event Loop continuously monitors the Call Stack. When the Call Stack becomes empty, it moves callbacks from the Callback Queue to the Call Stack, one at a time, for execution.
**/
setTimeout(function two() {
    console.log('Two Seconds!');
}, 2000);

setTimeout(function zero() {
    console.log('Zero Seconds!');
}, 0);

const p1 = new Promise((resolve, reject) => {
    resolve("p1 is resolved.")
})

const p2 = new Promise((resolve, reject) => {
    reject("p2 is rejected.")
})

p1.then((data) => {
    console.log(data)
}).catch((err) => {
    console.log(err)
})

p2.then((data) => {
    console.log(data)
}).catch((err) => {
    console.log(err)
})


console.log('Starting Up!');

console.log('Finishing Up!');

/* Output:
 Starting Up!
 Finishing Up!
 p1 is resolved.
 p2 is rejected.
 Zero Seconds!
 Two Seconds!
**/


/* Control Flow:
 1. The entire script is loaded, and the global execution context is pushed onto the Call Stack.
 2. Line 1: setTimeout is pushed onto the Call Stack. The JavaScript engine removes it from the stack and registers it with the Web API, where a timer of 2 seconds begins. After 2 seconds, its callback is placed into the Callback Queue.
 3. Line 5: Another setTimeout is pushed onto the Call Stack. It is then handed over to the Web API, where a timer of 0 seconds starts. Once the 0-second timer completes, its callback function is added to the Callback Queue.
 4. Line 9: Promise (p1) is created and pushed onto the Call Stack. It is then handed over to the Web API, where it gets resolved.
 5. Line 13: Promise (p2) is created and pushed onto the Call Stack. It is then handed over to the Web API, where it gets rejected.
 6. Line 17: Since p1 is resolved, callback function inside .then() is added to the Microtask queue, while the .catch() handler is ignored.
 7. Line 23: Since p2 is rejected, the callback inside .catch() is added to the Microtask Queue, while the .then() handler is ignored.
 8. Line 30: console.log is pushed onto the Call Stack, executed immediately, and then removed.
 9. Line 32: console.log is pushed onto the Call Stack, executed, and then removed.
 10. The Event Loop continuously checks whether the Call Stack is empty. When it is, the Event Loop processes all callbacks in the Microtask Queue (until it is empty) before moving on to callbacks in the Callback Queue.
**/

Summary of Execution Order:

  • Synchronous code executes first (added to the call stack).

  • Once the call stack is empty, Microtasks (e.g., resolved promises) are handled.

  • After the microtask queue is cleared, Macrotasks (e.g., setTimeout, I/O callbacks) are processed from the Callback Queue.


What all operations can be considered asynchronous in javascript?

In JavaScript, asynchronous operations are those that don’t block the execution of the main thread (call stack). Instead, they are handled in the background (via Web APIs / Node APIs) and their result is processed later using Callback Queue, High Priority Queue, and Event Loop.

Common Asynchronous Operations in JavaScript

  1. ⏱️ Timers (setTimeout, setInterval)

  2. 🌐 Network Requests (API Calls)

  3. 🎧 Event Listeners

  4. 🧡 Promises & Async/Await

  5. πŸ“ File System Operations

  6. πŸ—„οΈ Database Operations

  7. πŸ“‘ Web APIs / Browser APIs

  8. πŸ’¬ WebSockets / Real-time Communication

  9. 🎨 requestAnimationFrame

  10. βš™οΈ Workers (Web Workers / Worker Threads)


πŸ’‘
Difference between Synchronous Code vs Asynchronous Code
Synchronous Code Asynchronous Code
Line-by-line execution.
Consumes the main thread. Doesn't consume the main thread. Runs in the background.
Blocking in nature. Non-blocking in nature.

Happy reading!