From Good to Great: Why True Engineers Understand JavaScript Inside Out

The nuances of JavaScript’s Runtime Environment, and why every top engineer should be intimately familiar with them.

Yuri Bett
JavaScript in Plain English

--

True Engineers Understand Javascript under the hood
Photo by Peter Gombos on Unsplash

Once, during a job interview where I was on the interviewer’s panel with a few colleagues, one of them posed a question to the candidate:

“How does JavaScript work under the hood? Can you briefly explain what you understand about runtime elements like the heap, call stack, event loop, and so forth?”

I was impressed that such a topic remains a mystery for many JavaScript engineers. And, frankly, one cannot truly claim seniority without having some idea about this subject.

To write high-quality code, it’s essential to comprehend how that code runs.

Always bear in mind: “Your coding can only be as good as your understanding.”

So, could you answer that interview question?

JavaScript, which holds a significant presence in the global programming world, functions based on a unique execution model.

Thanks to Miro (my first attempt at creating my own diagrams), I’ve illustrated the Runtime Environment as follows:

Javascript Runtime Environment

Now let's understand each part of it.

JavaScript’s Execution Environment (Engine)

The execution environment, frequently referred to as the JavaScript engine, is a complex space where all the magic of running your JavaScript code happens. Various browsers and platforms have their specialized engines: V8 powers Google Chrome and Node.js; SpiderMonkey is behind Firefox; JavaScriptCore runs inside Safari.

Within this execution environment, two primary components play important roles in how your code gets executed: the Heap and the Call Stack.

The Heap:

  • Nature: It is a region of your computer’s memory that is unstructured and provides space for storing variables and instances that your program creates.
  • Dynamics: When you instantiate a new object or declare a large array, for instance, these are stored in the Heap. This allocation happens dynamically, meaning space is allocated or de-allocated as needed during program execution.
  • Garbage Collection: One significant aspect of the Heap is the “Garbage Collection” mechanism. This automatic process identifies when memory is no longer in use and reclaims it. It ensures that applications don’t consume memory that isn’t being used, which can otherwise lead to memory leaks and inefficiencies.

Talking about Garbage Collection, if you want to understand more about the topic and how to avoid memory leaks, I have another post that will help you:

The Call Stack:

  • Nature: The Call Stack is a Last-In-First-Out (LIFO) data structure that records the point in the program where operations are at — basically, where functions are called so that execution can return to the correct location once those functions have been executed or if an error is thrown.
  • Dynamics: When you invoke a function, a new frame (representing that function’s execution context) is pushed onto the Call Stack. As the function completes its execution, its frame is popped off the stack, and control returns to where it was invoked.
  • Stack Overflow: If the Call Stack has too many frames (due to, say, a recursive function that never terminates), it can lead to a stack overflow, and the browser will throw an error.
  • Single-Threaded Nature: It’s essential to note that JavaScript is single-threaded. This means that only one operation is processed at any given moment. If a function is being executed, it occupies the Call Stack until it’s done, blocking any other function from executing.

For illustrative purposes:

function multiply(x, y) {
return x * y;
}

function calculate() {
const value = multiply(5, 3);
console.log(value);
}

calculate();

In this example:

  1. The calculate() function is called, placing its frame on the Call Stack.
  2. Inside calculate(), the multiply() function is invoked, adding its frame to the top of the stack.
  3. Once multiply() completes, it returns the value 15, and its frame is removed from the Call Stack.
  4. The calculate() function continues its execution and logs the value 15 to the console. After it finishes executing, its frame is also popped off the Call Stack.
Javascript Call Stack

JavaScript’s Asynchronous Mechanics: Web APIs, Callback Queue, and the Event Loop

In a synchronous world, each instruction must wait for the previous one to complete. However, to deal with operations that might take unpredictable amounts of time (like reading a file or fetching data from a server), JavaScript employs a non-blocking, asynchronous model. This model is made efficient through a combination of Web APIs, the Callback Queue, and the Event Loop.

Web APIs:

  • Nature: These are functionalities that the browser (or the environment, in the case of Node.js) provides, which are outside the JavaScript engine but can be accessed using JavaScript.
  • Purpose: Web APIs handle tasks that would typically be blocking operations if run on the JavaScript engine directly. For instance, timers (setTimeout and setInterval), AJAX calls (like fetch), and DOM manipulation tasks are managed here.
  • Interaction: Once a Web API task completes, its callback function is sent to the Callback Queue, ready to be executed.

Callback Queue (or Task Queue):

  • Nature: As the name suggests, it’s a queue (First-In-First-Out structure) that holds all the callback functions that are ready to be executed after their corresponding Web API tasks complete.
  • Dynamics: Callback functions are lined up in this queue in the order in which their associated tasks finish in the Web API. However, they don’t automatically move to the Call Stack. That’s the job of the Event Loop.

Event Loop:

  • Role: Its primary role is to monitor both the Call Stack and the Callback Queue. If the Call Stack is empty and there’s a function waiting in the Callback Queue, the Event Loop dequeues it and pushes it onto the Call Stack to be executed.
  • Ensuring Non-blocking Behavior: The Event Loop ensures that JavaScript remains non-blocking. Even if an asynchronous operation takes a long time in the Web API, other functions can still run and complete in the Call Stack.

Consider the sequence:

console.log('First');
setTimeout(function() {
console.log('Second');
}, 0);
console.log('Third');

What do you think is the output of this code?

  1. console.log('First') is added to the Call Stack and executed.
  2. setTimeout is encountered. The timer operation is handed over to the Web APIs.
  3. Immediately after, console.log('Third') is added to the Call Stack and executed.
  4. Even though the timer duration is 0 milliseconds, the callback of setTimeout (which logs 'Second') is placed in the Callback Queue.
  5. The Event Loop, noticing the Call Stack is empty and there’s a function in the Callback Queue, transfers the callback to the Call Stack.
  6. Finally, console.log('Second') is executed.

Then the output of this code will be

First
Third
Second

This entire mechanism ensures that even for asynchronous code, the program execution flow remains consistent and non-blocking.

Chek how the diagram illustrates this case:

Javascript Runtime Environment with focus on Web API

And What About the Microtask Queue?

  • Role: The Microtask Queue contains a list of microtasks, which originate from promises, MutationObserverand other specific asynchronous operations.
  • Priority: Microtasks have a higher priority than tasks. So, after each task is executed, the JavaScript runtime checks the microtask queue, and if there are any pending microtasks, it executes all of them before moving on to the next task from the Callback Queue.

In order to understand, consider this example:

console.log('Start');

setTimeout(() => {
console.log('setTimeout');
}, 0);

Promise.resolve().then(() => {
console.log('Promise');
});

console.log('End');

What would you think the output would be?

Here it is:

Start
End
Promise
setTimeout

This order is because, after executing the synchronous code (Start and End), the event loop sees there’s a promise in the microtask queue and executes that before the setTimeout in the task queue, even though the setTimeout has a delay of 0.

Let's look at the diagram. Please note that the Promise is added by the Javascript Engine now since it is a direct promise, not created by fetch, that case it would go first through Web API.

Javascript Runtime Environment with focus on Web API and Promises

My Final Advice to You

As we wrap up, here’s something I’ve learned from years of working with JavaScript: being a great developer isn’t just about writing code. It’s about really understanding how that code works.

When you get how things like the event loop, microtask queue, and call stack work, you’ll feel more connected to your code. Every piece of code you write will make more sense because you know what’s happening behind the scenes.

For those who are new to all this, it might feel a bit tough at the start. And that’s completely fine. Learning takes time. But as you spend more time with JavaScript, don’t just try to remember things. Instead, try to picture how everything works together. This will help you solve problems and understand weird things that might happen in your code.

So, if you’re just starting or even if you’ve been coding for a while, take the time to really understand how JavaScript works. The more you know about the foundations, the easier coding will feel. And always remember: it’s important to understand your tools to use them well.

Let’s get connected! You can find me on:
- Medium: https://medium.com/@yuribett
- Linkedin: https://www.linkedin.com/in/yuribett/
- X (formerly Twitter): https://twitter.com/yuribett

In Plain English

Thank you for being a part of our community! Before you go:

--

--

Senior Software Engineer | Technical Lead | Technical Writer - I love everything about Javascript, React.js, Next.js, and Node.js