JavaScript Event Loop: The Heartbeat of Asynchronous Programming

09 / Sep / 2024 by Deepesh Agrawal 0 comments

JavaScript, despite being a single-threaded language, handles multiple tasks simultaneously through its powerful event loop. Whether you’re fetching data from an API, handling user interactions, or processing large datasets, the event loop ensures that your application remains responsive and efficient. But how does this process work under the hood? In this blog, we’ll find out how the JavaScript event loop works with asynchronous operations. By the end of this article, you’ll have a clear understanding of the event loop’s role in JavaScript and how you can leverage it to write more efficient and performant code.

1. The Basics:

Call Stack: The call stack is a LIFO (Last In, First Out) data structure that records the current point of execution in your code. Whenever a function is invoked, it’s pushed onto the stack, and when it’s finished, it’s popped off the stack.

How Call Stack works:

JavaScript is a synchronous, single-threaded language. When a JavaScript program runs, a global execution context is created and pushed onto the call stack.

function hello() {
console.log("hello there");
}
hello();
console.log("end")
Execution of Program

How program is getting executed

In the global execution context (GEC), the entire code is executed line by line. First, the definition of the hello function is encountered, and memory is allocated for it. The next line is a function invocation hello(). When this function is invoked, a new execution context is created for it. This context for ‘hello’ function is pushed onto the call stack.

As the hello function executes, its code runs line by line, printing “hello there” to the console. Once the hello function has finished executing, its execution context is popped off the call stack. Control then moves to the next line of the code, which is console.log(“end”).

After executing console.log(“end”), there are no more lines of code left to execute, so the global execution context is also popped off the call stack.

Output :

hello there
end

Call Stack & Web API's Diagram

Call Stack & Web API’s Diagram

Web APIs:

In the browser environment, JavaScript has access to various Web APIs, such as setTimeout, DOM events, and fetch. When these APIs are used, they handle operations asynchronously. These APIs are accessible through the window object, although you can use them directly without explicitly referencing the window object. Internally, they interact with the window object.

For example, when using setTimeout(), it sets up a timer. Once the timer completes, it places the callback function into the callback queue. We’ll explore how this process works in more detail in the upcoming examples.

Event Loop:

The event loop is a continuous process that constantly monitors the call stack. If the call stack is empty, it first processes tasks from the microtask queue (such as promises and mutation observers) before moving on to tasks from the task queue (such as setTimeout and I/O tasks). The event loop ensures that tasks are executed in the correct order and that the application remains responsive.

How Event Loop comes into the Picture:

console.log("start")
setTimeout(function cb() {
console.log("Callback Executed");
}, 2000); 

console.log("end")

Output:

Start
end
Callback Executed

The setTimeout function registers a timer with a delay of 2000 ms and also registers a callback function. As the timer runs asynchronously, JavaScript does not wait for it to complete and moves on to execute the next line of code, printing “end”. Once there are no more tasks in the call stack, the global execution context is popped from the call stack.

At this point, the callback function for setTimeout is still pending for execution. This is where the event loop comes into play. When the timer expires, the callback function is added to the macro task (or task, or callback) queue. The event loop continuously checks the call stack and, when it finds it empty, pushes the callback from the macro task queue to the call stack for execution.

You might wonder why the event loop is necessary and why we can’t directly push the callback into the call stack. The event loop acts as a mediator because there are different types of registered callbacks with varying priorities. Based on their type, callbacks are placed into either the macro task (callback) queue or the microtask queue. The event loop then picks and processes these callbacks based on their priority.

Microtask Queue: Microtasks (e.g., resolved Promises, mutation observers) have higher priority and are processed before tasks in the Task Queue. After each task in the call stack is completed, the event loop will process all pending microtasks until the queue is empty. Only then will it move on to the next task in the Task Queue.

Task Queue (Callback Queue): Tasks (e.g., setTimeout, setInterval, I/O tasks) are processed only after the Microtask Queue is empty. This queue is also known as the Macrotask Queue.

console.log("start")
setTimeout(function cb() {
  console.log("SetTimeout callback Executed");
}, 2000); 

fetch('https://api.example.com/data') // Replace with valid url
  .then(response => response.json())
  .then(data => {
    console.log('API Call Executed', data);
  }).catch(error => {
    console.error('Error:', error);
  });

console.log("end")

There are two asynchronous operations: setTimeout and fetch.

The setTimeout callback will be placed in the Task Queue (or Callback Queue).
The fetch call returns a promise, which, when resolved, will place its callback in the Microtask Queue.

Suppose both queues have pending tasks at the same time, The event loop will prioritize and process the Microtask queue first. Once the Microtask Queue is empty, then it  will process tasks in the Task Queue.

Event Loop and Queue Processing Flow

Event Loop and Queue Processing Flow

Output: Depends on API response (Assuming API response comes in less than 2 sec)

start
end
API Call Executed { … } // (or an error message)
SetTimeout callback Executed

Conclusion

Understanding the JavaScript Event Loop is essential for mastering asynchronous programming. The Event Loop is the heart of JavaScript’s concurrency model, allowing non-blocking I/O operations and ensuring that even complex applications remain responsive. By distinguishing between the Call Stack, Microtask Queue, and Callback queue and understanding how they interact, developers can write more efficient, predictable, and bug-free code. As you learn more about JavaScript, understanding how the Event Loop works will help you make your applications faster and more user-friendly.

FOUND THIS USEFUL? SHARE IT

Leave a Reply

Your email address will not be published. Required fields are marked *