JavaScript in depth: execution contexts and event loop (3 of 4).

Luca Di Molfetta
4 min readMar 16, 2024

--

How event loop works and how it interacts with the JavaScript engine.

This is the third part of a four article mini-series. You can read the first part here and the previous part here.

Introduction

Before starting, let’s summarize the key points we analyzed in the previous part that will be useful to better understand this article:

  1. JavaScript is a single-thread programming language, meaning it can execute only one instruction at a time.
  2. The process responsible for actually executing JS code is called JavaScript engine, and it consists of two main components, the Heap (where references to objects and data structures are dynamically stored) and the Call Stack, which keeps tracks of the order of instructions that need to be executed.
  3. The JavaScript engine is in turn part of an even larger process handled called the JavaScript runtime environment, in which there are some extra-functionalities (in a browser the Web APIs) to which the JavaScript engine delegates management.
  4. One of the primary responsibilities of the JavaScript runtime is to ensure that asynchronous operations, such as server requests, do not block the execution thread. Failure to do so, particularly in a JavaScript runtime like a browser, can result in unresponsive web pages, making your users frustrated and upset.
  5. In order to achieve the goal of the previous point, a process called Event Loop runs continuously inside the JavaScript runtime environment to coordinate the execution of synchronous and asynchronous tasks inside the JavaScript engine, enabling a concurrency model.

How does the event loop work?

An animated representation of the interactions between the Call Stack (JS Engine), the Web APIs and the event loop.

Beginning with the most fundamental explanation, when a JavaScript runtime is instantiated, the Event Loop is created and begins running alongside other components such as the JavaScript engine, the DOM, and all other Web APIs. Among these components, there is another crucial data structure we have not yet mentioned: the Task Queue, sometimes also referred to as the Callback Queue (referring to picture above is “Queue”).

We can define the Task Queue as the container of the asynchronous operations that are ready to be executed. So the job of the event loop is to continuously monitor both the Call Stack of the JavaScript engine and the Task Queue. If there are elements (callbacks) in the Task Queue ready to be executed, the event loop pushes them in the call stack for execution. The elements in the Task Queue are processed with a FIFO (First In, First Out) approach

The event loop iterates over an ordered list of phases, which depend on the specific JavaScript runtime environment. For example, the phases of the event loop in Node.js differ from those in the Google Chrome browser. Each iteration of the cycle is technically defined as a “tick”. An important concept to understand is however that the callbacks added to the Task Queue after an iteration of the event loop starts won’t run until the next iteration of the event loop happens.

Look at the following code for example:


//Simulate a fetch request that will take 1 second for the response to arrive
const simulateCallToServer = async () => {
const response = new Promise((resolve) => setTimeout(() => {
resolve("Data arrived!"), 1000));
}

console.log(await response);
}

simulateCallToServer();

//Simulates an event listener attached to a button
document.addEventListener("userClickedAButton", ()=> {
console.log("The user has clicked something!");
})

//The setTimeout will put the callback in the Task Queue, even if the timer
//is 0
setTimeout(() => {
document.dispatchEvent(new Event("userClickedAButton"));
}, 0)

//The for loop will run synchronously, causing the web page to block
//until the loop ends
for (const num of Array.from({length: 1000}).map((_, index)=> index) {
console.log(num)
}

console.log('I have to wait the end of the for loop to be executed');

When the code runs the following things happen:

  1. When the simulateCallToServer() function is called, since it is a Promise, it will be handled asynchronously. Until the promise is pending, the JavaScript engine will keep track of its status internally without making any further operation.
  2. When the "userClickedAButton" event is fired, since it is wrapped inside a timer, it is pushed into the Task queue. Timers such as setInterval()and setTimeout() are always sent to the Task queue, even if the timer duration is 0. Furthermore, since an iteration of the event loop is already running, it will be executed as soon as the Call Stack of the JavaScript engine is empty (after the last console.log()).
  3. The for loop is executed synchronously, and the JavaScript engine will wait until the end of the for loop to execute the next statement.
  4. After the console.log("I have to wait the end..."), the call stack is now empty, a new tick of the event loop runs. The event listener userClickedAButton can finally be executed, and the event loop pushes it into the JavaScript engine call stack, printing "The user has clicked something” to the console.
  5. What happened to the simulateCallToServer() promise? Since the timer hasn’t elapsed yet, it has to wait at least 1000ms before being inserted into the Task Queue. At that point, since the Call Stack is already empty, it will be immediately processed, printing "Data arrived!" as the last statement of the code.

Now that we understand how asynchronous programming works in JavaScript, In order to be fully confident in solving the problem that was seen in the part 1, we need to analyze in depth how the JavaScript engine’s Call Stack stack works. We will see that it operates with a logic diametrically opposite to FIFO, and each element inside the call stack has a hierarchical relationship used to resolve scope, variables, and references to the outer lexical environment. I hope to see you there!

Side Notes

The general architecture presented in this article has been slightly simplified in order to make you understand the basics of how JavaScript handles asynchronous code. In reality, there is more than one Queue in which asynchronous callbacks are pushed, but the way the event loop works remains pretty much the same. If you want to dive deeper you can read this resource.

--

--

Luca Di Molfetta

I'm a former Italian Navy Officer who switched careers to become a developer. I'm working as Angular developer @RED (https://red.software.systems/en/home)