Understanding the JavaScript Event Loop

Mon Sep 09 2024

~ 4 mins

Introduction

JavaScript is a powerful and easy to lean language that anyone can master, but to write efficient and bug-free code, it's essential to understand how JavaScript executes your code behind the scenes.

One of the most crucial components in this process is the event loop and plays a key role in managing how tasks are handled.

What is a JavaScript Runtime?

Before diving into the event loop, it's important to understand what a JavaScript runtime is.

A JavaScript runtime is the area in your computer (environment) where your js code is executed.

It contains everything your computer needs needed to translate and execute your code such as a js engine, any third party software, and of course the event loop.

How the JavaScript Engine Runs

When the JavaScript engine runs your code, it does so in a synchronous, single-threaded manner. This means that the engine executes one line of code at a time, from top to bottom.

This might seem limiting, but JavaScript is also designed to handle asynchronous tasks, which are tasks that can run independently from the main program.

Synchronous vs. Asynchronous Code

The js engine reads your code line by line, from top to bottom.

  • Synchronous code is the code that gets executed by the js engine as soon as it read.
  • Asynchronous code is the code that the js engine sends somewhere else to be executed instead of doing it itself. e.g. setTimeout

These asynchronous tasks get passed to the APIs and libraries provided by the runtime. These tasks can then run independently, which frees up the engine to continue executing the rest of your code.

When the API's and libraries have finished executing, any function you wanted to be ran upon completion (a.k.a a callback function) gets added into a task queue.

The main job of the event loop is to:

  1. Monitor the js engine's call stack
  2. Monitor the task queue
  3. Send tasks to the js engine when it is free

The Call Stack and Task Queue

The callstack is what keeps track of the function that the js engine is currently executing.

Synchronous code get sent to the callstack to be executed as soon as its read .When the call stack is empty, it means that all the synchronous code has been executed.

The event loop constantly checks the task queue and the call stack, pushing tasks to the stack whenever it's empty.

Event loops

While the event loop's basic functionality remains consistent across environments, different JavaScript runtimes have variations in how they handle tasks.

Each runtime has its own JavaScript engine and task queues, and the event loop might pull tasks from these queues in different orders depending on the runtime.

Chrome event loop

In Google Chrome the js runtime is powered by the V8 engine. The V8 runtime uses two types of task queues: the macrotask queue and the microtask queue.

  • Macrotask Queue: e.g. setTimeout, setInterval
  • Microtask Queue: e.g. Promise callbacks

The event loop in the browsr ensures that the microtask queue is always empty before picking a task from the macrotask queue.

This means that after every macrotask is executed, the event loop checks if there are any microtasks in the microtask queue. If there are, it executes them one by one until the queue is empty before moving on to the next macrotask.

This behaviour ensures that microtasks are prioritized, allowing for more efficient handling of certain operations.

Node.js event loop

Node.js, another popular JavaScript runtime, is used primarily for server-side programming.

Like the browser runtime, Node.js also has both a microtask and a macrotask queue. However, these queues are divided into smaller queues, adding a bit more complexity.

  • Microtask Queue: This is split into two separate queues:
    • nextTick queue - Handles callbacks for process.nextTick()
    • Promise queue - Handles promise callbacks.
  • Macrotask Queue: This is divided into multiple queues, each with a specific purpose:
    • Timer Queue - Handles callbacks from setTimeout and setInterval.
    • I/O Queue - Handles callbacks related to I/O operations like reading files or network requests.
    • Check Queue - Handles callbacks from setImmediate().
    • Close Queue - Handles cleanup tasks, such as closing file descriptors or sockets.

In Node.js, the event loop operates similarly to the browser. It clears the microtask queue first, ensuring that all nextTick and Promise callbacks are executed before moving on to a macrotasks.

Then, the event loop processes each macrotask queue in order, starting with the timer queue and ending with the close queue. Each queue is cleared one by one before the event loop moves on to the next queue.

This design allows Node.js to efficiently manage server-side tasks, such as handling multiple client requests simultaneously without blocking the main thread.

Conclusion

In summary, the event loop is an integral part of the JavaScript runtime, coordinating tasks and managing the execution of code.

Whether you're working in a browser environment or using Node.js, the event loop enables JavaScript to handle asynchronous tasks efficiently by leveraging different task queues and prioritizing operations.

By understanding how the event loop works, you can write better code that fully utilizes JavaScript's capabilities.