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

Luca Di Molfetta
6 min readMar 9, 2024

--

An example of the only type of v8 engine that I knew before this article

This is the second part of a four article mini-series. If you missed the first part, read it here.

Introduction

As a Frontend Developer, I think that, before trying to master the latest generation framework or trend technology, it’s important to understand how a browser works; and trust me, it’s a very hard topic to fully understand (if you want to give it a try I would suggest this resource).

In this article I will try to explain the general architecture behind one of the main parts of any browser, the one related to JavaScript code execution (handled by the JavaScript engine) and its interactions with browser functionalities (the Web APIs) inside the broader process of the browser that contains both (the Javascript Runtime).

A general overview of a JavaScript runtime environment

I will keep it as easy as I can, and I hope you will learn something useful. Let’s start from the core…The JavaScript Engine!

JavaScript Engine: definition and purpose

In order to be able to run JavaScript code, as in any programming language, you need a software that, at the most basic level, performs these operations:

  1. Reads the source code line by line, character by character, and breaks it down into several pieces (technically called tokens) according to the syntax defined by the programming language itself. This phase is called lexical analysis.
  2. Creates a hierarchical structure of the variables and functions declared in the source code, in order to be able to generate the different execution contexts during the following phases. At the very minimum, there would be one execution context, the global one. This phase is called parsing.
  3. Converts the source code into binary code that could be executed by the machine or by another software. What happens next really depends on the specific programming language that you are working with, because at this point, you can either:
  • Save the converted output into a binary file for future execution, making your programming language a compiled one. Consider, for instance, the handling of a C++ source file.
  • Immediately run the code as soon as it gets converted into binary, as it happens in the case of an interpreted programming language. This is roughly how JavaScript works (in reality the process performed is called Just-In-Time compilation, but this is not meaningful for the scope of this article).

In the JavaScript world, any software that is responsible for executing the steps described above for a .js file is called a JavaScript Engine. There are several JS engines available, the most famous one is the v8 engine, implemented inside Chrome browser and Node.js, but there is also Spidermonkey (used by Firefox) or Chakra (used by IE and Edge). They are written in different ways and potentially using different low level programming languages, but the final goal remains the same: to parse and execute JS code.

At this point you may ask yourself a very important question:

If there are many JavaScript engines out there, how do I have a uniform behavior when I execute the same JS source in different browsers?

Because each vendor tries to adhere to a standard, aka the ECMAScript standard. As mentioned by the Ecma International Organization:

ECMAScript® is the scripting language that is used to create web pages with dynamic behavior. ECMAScript®, which is also commonly known by the name JavaScript™, is an essential component of every web browser and the ECMAScript® standard is one of the core standards that enables the existence of interoperable web applications on the World Wide Web.

Every time you update your browser, it is possible that you are installing a newer version of the related JavaScript engine, that is constantly evolving in order to adapt to the latest ECMAScript standard.

caniuse.com is the most used reference to check what version of each browser implements a particular functionality of the ECMAScript standard. In this picture for example, we can see the the method Object.groupBy is available only for the versions highlighted in green.

JavaScript Engine: general architecture

Okay, now that we have defined what is a JavaScript engine, let’s try to understand generally how it works. Even if the actual implementation could vary across the different engines, there are some key elements and characteristics in common that will be useful to understand the solution of the problem seen in part 1:

  1. It is single-threaded, meaning that it executes code sequentially, one statement at a time, within a single thread of execution (in the browser it could be a tab for example, so each tab has an instance of the JavaScript engine it implements). This means that only one operation is performed at any given moment, and subsequent operations wait for the previous one to complete before they can be executed. We will see in the next part that in reality this limitation could be overcome.
  2. Includes two main components, the call stack and the heap (see introduction image). The call stack is a data structure that keeps track of the function that is being executed and of the subsequent ones. Every element of the call stack corresponds to an execution context (more on that in the next article), that contains information such as the function’s arguments, local variables and the location in the code where the function was called. The heap instead is a dynamic allocated region of memory that stores the address (not the value!) of objects, arrays and other data structures created during a function execution.

This is interesting, but the real question is:

Even if I know that the JavaScript Engine is responsible for executing JS scripts, what happens when it we have a statement such as document.createElement("div") or when we try to call an external API with a method like fetch('myAPIurl')?

These functions generate effects that are beyond simple code execution, such as creating a new element in the web page or even reaching an external server to retrieve/send data. This is where the surrounding Javascript Runtime Environment (in our case a browser) and the interfaces it exposes to the JS Engine (in this example the WEB APIs such as the DOM or the fetchAPI) come into play.

The most common JavaScript runtime environment: the browser and its APIs

Similar to what happens in a car, where the engine alone is not enough to make the car move, because it needs to be connected to other components, in order to provide its functionalities, a JavaScript engine needs a context in which to operate. This context is the JavaScript runtime environment, and in the case we are examining, it is the browser.

From a hierarchical perspective, we can say that the JS runtime is the parent process that includes both the JS Engine as well as all the other functionalities that a web browser usually offers, such as the Document Object Model to add interactivity and style to a web page, the Storage API, the Geolocation API, the Fetch API, and so on.

A web browser is not the only JavaScript runtime environment that can exist. A well known alternative is Node.js, that according to the official website is defined as follows:

Node.js is an open-source and cross-platform JavaScript runtime environment. It is a popular tool for almost any kind of project!

Node.js runs the V8 JavaScript engine, the core of Google Chrome, outside of the browser. This allows Node.js to be very performant.

Just to make a very simple example. If you try to run the following script.js with Node.js:

const div = document.createElement("div");
div.textContent = "Hello World";
document.body.appendChild(div);

It will fail, giving you the following error: ReferenceError: document is not defined. This happens because the document is a functionality external to the JS Engine and it’s provided only by a browser runtime environment.

But a JS runtime does way more than providing extra power to the JS engine.

One of the most important aspects it handles is managing asynchronous operations and events. As we said before, even if the JS Engine is single threaded, in reality it is perfectly able to execute multiple tasks in parallel. This result is possible through a process that is part of any JavaScript runtime, called Event Loop.

But this will be the subject of the next article…See you there!

--

--

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)