DEV Community

Adel Stiti
Adel Stiti

Posted on

How JavaScript Works: The Runtime Environment

A program is fundamentally about two things:

  1. Allocating Memory: Where do we store our data and variables?
  2. Parsing and Executing: How do we read and run our instructions?

To run our JavaScript code, we need an engine that translates it into machine code. Browsers have different engines, like V8 in Chrome and SpiderMonkey in Firefox. For server-side execution, we use environments like Node.js, which is built on the V8 engine.

The JavaScript Engine

JavaScript Engine Illustration

The JS Engine is the "heart" where your code is actually understood and executed. Inside the engine, we have two key parts:

  • Memory Heap: This is where memory allocation happens. When you create variables, objects, or functions, they get allocated space in the heap. This is why we need to be mindful of memory leaks. For example, by avoiding unnecessary global variables that never get cleared.
  • Call Stack: This is where your code is read and executed, line by line. It's a stack data structure that records where we are in the program. Each function call is pushed onto the stack, and when it returns, it's popped off.

JavaScript is a single-threaded language. This means it has only one Call Stack. It does one thing at a time, finishing one task completely before starting the next.

This single-threaded model simplifies development by avoiding the complexities of multi-threaded environments, such as deadlock scenarios common in languages like Java or C++.

But this leads to a critical question...

The Single-Threaded Paradox: How is JavaScript Non-Blocking?

If JavaScript has only one call stack, how can it perform asynchronous operations like waiting for a setTimeout or fetching data from an API without "blocking" the main thread and freezing the UI?

The answer is that the JavaScript Engine doesn't work alone. It's part of a larger system called the JavaScript Runtime Environment.

The JavaScript Runtime Environment: The Orchestrator

JavaScript Runtime Environment

The runtime environment is what makes JavaScript powerful and asynchronous. It's composed of several parts working in harmony:

  • The JS Engine (Heap & Call Stack): The core we just discussed.
  • Web APIs: These are features provided by the browser, like the DOM, setTimeout, fetch (or XMLHttpRequest), and event listeners. They are not part of the JavaScript language itself.
  • Callback Queue (or Task Queue): This is a "First In, First Out" (FIFO) queue. When a background Web API task (like a timer or network request) is finished, its callback function is placed here, waiting to be run.
  • The Event Loop: This is the manager that coordinates everything. Its job is simple: "Is the Call Stack empty? If yes, take the first item from the Callback Queue and push it onto the Call Stack."

Note: There is also a Microtask Queue (for Promises, like fetch(), and async/await) which the Event Loop checks before the Callback Queue. This is why .then() or await often seems to run "faster" than setTimeout.

How Asynchronous JavaScript Works (Step-by-Step)

Let's trace what happens when we run an asynchronous function like setTimeout:

console.log('Start');

setTimeout(function myCallback() {
  console.log('Inside Timeout.');
}, 1000);

console.log('End');
Enter fullscreen mode Exit fullscreen mode
  1. console.log('Start') is pushed onto the Call Stack. It runs, prints "Start", and is popped off.
  2. setTimeout(...) is pushed onto the Call Stack.
  3. setTimeout is recognized as a Web API. The engine tells the browser to "start a 1-second timer. When you're done, take myCallback and put it in the queue."
  4. The setTimeout call itself finishes immediately (its only job was to hand off the timer to the browser). It is popped off the Call Stack.
  5. console.log('End') is pushed onto the Call Stack. It runs, prints "End", and is popped off.
  6. The Call Stack is now empty. The main script is finished.
  7. ...In the background, the browser's timer is still ticking...
  8. After 1 second, the timer finishes. The browser takes myCallback and places it in the Callback Queue.
  9. The Event Loop is always checking. It sees the Call Stack is empty and there's something in the Callback Queue.
  10. The Event Loop moves myCallback from the Callback Queue to the Call Stack.
  11. The engine executes myCallback code. console.log('Inside Timeout.') is pushed to the stack, prints "Inside Timeout", and is popped.
  12. myCallback finishes and is popped. The Call Stack is empty. The program is complete.

Output:

Start
End
Inside Timeout.
Enter fullscreen mode Exit fullscreen mode

Recap: The Power of the Runtime

JavaScript is a single-threaded language, meaning it has only one Call Stack and can execute only one task at a time. It achieves non-blocking behavior by delegating slow operations - like timers or API calls - to the browser's Web APIs, which handle them in the background. Once a background task completes, its callback function is placed in the Callback Queue. The Event Loop constantly monitors the Call Stack, and the moment it becomes empty, the Event Loop takes the first callback from the queue and pushes it onto the stack for execution. This mechanism ensures the main thread is never blocked.

Thank you for reading! If you found this article helpful, please give it a ❤️ and 🦄! Feel free to share your thoughts in the comments, and let's connect on LinkedIn — I'd love to hear your insights!

Top comments (0)