Exploring the inner workings of a JavaScript runtime

JavaScript runtime diagram

As you can see in the above diagram, there are five significant components in a JavaScript runtime: the JavaScript engine, the web API environment, the microtask queue, the callback queue, and the event loop. Let's briefly learn about each of them.

JavaScript engine

The JavaScript engine is a major part of the runtime. The engine has two main components: the call stack and the memory. The main tasks of these two are to execute the code and store the data, respectively. Now, let's see how these two operate in detail.

Call stack

The call stack is where code execution happens in a JavaScript engine. The engine creates execution contexts, places them in the call stack, and the call stack executes the contexts.

In JavaScript, there are two types of execution contexts: the global execution context (GEC) and the function execution context (FEC). When a script runs, a GEC is created and placed at the bottom of the call stack. FECs, on the other hand, are created for each function call found in a script.

Once the execution contexts are created, the FEC for the first function call found in a script is placed on top of the GEC in the call stack. The call stack then executes the FEC and removes it from the stack.

After that, the next FEC is placed onto the call stack and undergoes the same execution process. This pattern continues until all the FECs in a script are executed. Now, let's see how the call stack behaves when one function is called within another function.

Let's say we have two functions in a script: logger and welcome, where welcome is called within logger as shown in the following example.

function logger() {
  console.log('logger');
  welcome();
}

function welcome() {
  console.log('welcome');
}

logger();

When this script runs, execution contexts are created for both logger and welcome functions. After that, the logger FEC is placed onto the call stack. Having the welcome function called within the logger function, the welcome FEC is placed on top of the logger FEC.

Having no other nested function calls, the call stack executes the welcome FEC, removes it from the stack, then executes the logger FEC, removes it from the stack, and reaches the GEC. This is how the call stack keeps track of the order of function calls and their respective execution contexts for nested functions.

As you can see, the call stack adheres to the Last In, First Out (LIFO) principle, executing the most recently added FEC first, removing it, and then moving on to the next FEC in the stack, and so on.

If you are interested in a deeper dive into the code execution process in a JavaScript engine, I have an article on A deep dive into the JavaScript code execution process that provides further insights.

Anyway, this process of code execution applies to synchronous code. What about asynchronous operations then? Here comes the coordination of the web API environment, the callback queue, the microtask queue, and the event loop. We will learn about each of them later in this article. Before that, let's take a look at the memory.

Memory

There are two types of memory: heap memory and stack memory. The tasks of these two are to store two different types of data.

Heap

Heap memory is used to store reference data types. Objects, arrays, dates, etc., are reference data types and are stored in the heap. A reference to the object is then created and stored in the stack memory to access the data in the object.

Stack

On the other hand, stack memory is used to store primitive data types. Strings, numbers, booleans, etc., are primitive data types and are stored directly in the stack memory.

If you're interested in a deeper dive into how these data types are stored and accessed in memory, I have an article on Primitive VS Reference data types in JavaScript that provides further insights.

Web API environment

One main task of the web API environment is to handle asynchronous operations. Unlike synchronous code, asynchronous operations don't get executed in the call stack right away.

Rather, they get moved and registered to the web API environment. In this environment, the promises wait to be settled (resolved or rejected), and the callback functions wait to be loaded.

Besides handling asynchronous operations, the web API environment also provides the web APIs, e.g., setTimeout, setInterval, etc., as the name suggests.

Now, let's see what happens when the promises are settled and the callback functions are loaded.

Callback queue

The callback queue is another vital part of a JavaScript runtime. Once a callback function is loaded (completion of a timer or I/O operation) in the web API environment, it gets moved to the callback queue.

The callback queue is kind of a waiting room for the callback tasks before they go to the call stack for execution. It operates by the principles of First In, First Out (FIFO), which means the first function that comes from the web API environment gets to the call stack first.

Microtask queue

The microtask queue, on the other hand, stores the promise tasks. Once a promise is settled in the web API environment, it gets moved to the microtask queue. This is kind of a waiting room for the promise tasks before they get to the call stack for execution. It also operates by the principle of First In, First Out (FIFO).

Though the microtask queue is quite similar to the callback queue, one important difference between them is that the microtask queue has priority over the callback queue. We will learn how the priority works in the event loop section of this article.

Now, let's take a look at how the event loop helps the microtasks and callback tasks to get executed in the call stack.

Event loop

The event loop is a mechanism that constantly checks whether the call stack is empty. As soon as it finds the call stack empty (all the synchronous code is executed), it checks if there are any tasks in the microtask queue (priority over the callback queue). If there are, it takes the tasks and places them in the call stack for execution.

Only when both the microtask queue and the call stack are empty, does the event loop start checking for tasks in the callback queue. It takes the tasks it finds in the queue and places them in the call stack for execution.

This ends the whole process of executing synchronous and asynchronous code. As you can see, the JavaScript engine doesn't execute these slightly different types of code simultaneously. Doing so, the asynchronous operations would block the execution, resulting in a much slower execution speed.

Rather, the runtime has separate environments for each type of code where they run simultaneously. This is called the concurrency model of JavaScript, which helps process synchronous and asynchronous code at the same time, though the language itself is single-threaded. This non-blocking behavior is crucial to building performant and modern web applications.

There you have it! This is how the JavaScript runtime works in short. I hope you have a better idea of how your code gets executed inside a runtime now.

Share this article with your friends

Copy URL

Elevate your JavaScript and freelance journey

Supercharge your JavaScript skills and freelance career. Subscribe now for expert tips and insights!