JavaScript's Event Loop: The Core of Non-Blocking I/O

Ever wondered how JavaScript manages to be so responsive and efficient, even in the face of multiple tasks? The answer lies in the mechanism called event loop. In this blog, we'll break down all the mysteries of the event loop, explaining how it works behind the scenes, how it manages asynchronous operations and ensures your code runs smoothly. Let's dive in and discover how this powerful tool keep your applications running seamlessly!

JavaScript's Single-Threaded Nature

JavaScript is a single threaded language and has only one call stack that manages the flow of tasks and executes JavaScript code one task at a time. The call stack is present inside the JavaScript engine.

Lets understand using visual representation:

const printName = () => {
 console.log("Medium")
}
printName();
const printName = () => { 
  console.log("Medium")
}
printName()
console.log("END TASK");

Output:

None
It represents the output of the above code.

Behind the scene in browser:

None
This image shows how call stack looks initially
None
This image shows how call stack looks when it starts executing code.
None
This image shows how call stack looks when it has two task in the stack for execution.
None
This image shows how call stack looks after executing the task on the top.
None
This image shows how call stack looks after the completion of all task.

Code execution in a call stack

In the above image, we can see the visual representation of code executing in the call stack.

  • In the first image, it represents the initial stage of a call stack when it has nothing to execute.
  • Once we run the above code, the task is pushed inside the call stack with an anonymous name (as represented in the second image). This anonymous task is the Global Execution Context in which a code gets allotted memory and then gets executed. This is the initial phase where nothing is being executed on the call stack.
  • The engine starts allotting memory and executing each line of the code in the global execution context. If it encounters a function invocation, it creates another execution context (to execute code inside the function definition) and pushes that execution context inside the call stack as shown in the third image. In our example printName is invoked and an execution context is created inside the call stack.
  • Once the code is being executed in the printName execution context, it pops from the stack and continues with the remaining code execution as we can see in the fourth image. Once all the code had been executed, the global execution context also gets popped from the stack leaving the call stack empty.

So the above example shows how JavaScript runs its code in a synchronous manner showing its single-threaded behaviour. However, through the event loop, it can handle asynchronous operations efficiently, giving the illusion of multitasking.

What is the Event Loop?

The event loop is a core principle that enables JavaScript's asynchronous behavior. It allows JavaScript to perform various asynchronous operations (like callbacks, promises and events) simultaneously, despite being single-threaded. This mechanism ensures that multiple operations can be handled efficiently, keeping our applications responsive and fluid. The event loop checks the call stack and callback queue (which handle asynchronous task) continuously so that it can handle multiple operations without blocking the main execution thread. Before diving deep into the working of the event loop, it is essential to understand a few key components:

  1. Call Stack (Execution Stack): This is where JavaScript keeps track of which code is currently being executed. When a function is invoked, it is pushed onto the stack and once the function completes, it pops from the stack.
  2. Callback Queue (Task Queue): When asynchronous tasks (such as timers or event handlers) are completed, their associated callbacks are moved to the callback queue, waiting to be executed. This queue holds the asynchronous tasks until call stack is empty.
  3. Micro-task Queue: Micro-task queue has the higher priority over the callback queue as it holds smaller task that needs to be executed immediately after executing the script. It consists of tasks like promises and MutationObserver (it checks if there is any mutation in DOM tree or not).
  4. Web APIs: It is provided by the browser to handle asynchronous operations such as setTimeout, DOM events, HTTP requests, etc. These operations are not part of the JavaScript engine itself but are accessible through the environment in which JavaScript runs.
  5. Event Loop: It is the mechanism responsible for coordinating the execution of the call stack, callback queue, and micro-task queue. It continuously monitors these components, processing tasks in a precise sequence to ensure that JavaScript code executes smoothly and efficiently. By managing these queues, the event loop allows JavaScript to handle asynchronous operations and maintain a responsive user experience.

How Event Loop Works

The event loop operates in a continuous cycle, following these steps:

  • Check the Call Stack: The event loop first checks if the call stack is empty. If not, it continues to execute the tasks on the stack.
  • Execute Micro-tasks: Once the call stack is empty, the event loop processes all the tasks in the micro-task queue. These tasks are executed before any tasks in the task queue.
  • Process the Task Queue: After the micro-tasks are handled, the event loop picks the first task from the task queue and pushes it onto the call stack for execution.
  • Repeat: The event loop continues this process indefinitely, ensuring that JavaScript can handle asynchronous operations efficiently.

Let's understand using a simple example:

console.log('Start')
setTimeout(() => {
  console.log('Timeout callback');
}, 0);
Promise.resolve().then(() => {
  console.log('Promise callback');
});
console.log('End');

Output:

None
It represents the output of the above code
  • The Event loop checks the call stack if it is empty or not. If not, it continues to execute the function in the stack. In our example, the call stack is not empty as it has console logs to print. Therefore, initially it will execute all the synchronous tasks like printing console logs.
  • Once all the synchronous tasks are done and the call stack gets empty, then the event loop checks the call stack. If it is empty, it gives the priority to micro-task queue and executes all the tasks (it is the promise callback in the above example) by pushing the processed task inside the call stack.
  • Once all the tasks has been executed in micro-task queue, the call stack pops the task and gets empty. The event loop checks the call stack again if it is empty. If so, it pushes all the processed tasks in the callback queue for execution inside the call stack.

Conclusion

The event loop is fundamental to JavaScript's ability to handle asynchronous operations. By harnessing the power of the event loop, developers can build applications that are highly responsive and capable of managing multiple tasks simultaneously without sacrificing performance. Mastery of the event loop is a key skill for anyone working with JavaScript, whether on web applications or server-side projects, and will significantly enhance their programming expertise.