Deep dive into JavaScript Runtime

Yugam GangarYugam Gangar
6 min read

Have you ever wondered what happens behind the scenes when your JavaScript code comes to life on the browser? The JavaScript runtime is the environment where the whole magic happens. It provides all the necessary components for executing the Javascript code. There is no Javascript without its runtime.

The JavaScript runtime is not limited to web browsers and may differ when used on the server. Node.js runs on a JavaScript runtime that doesn't have Web APIs. Instead, it features other tools to support asynchronous execution.

By the end of this article, you will better understand how JavaScript executes code, manages asynchronous tasks, and maintains performance. You will be more confident in debugging, writing more efficient code and JavaScript interviews.

Components Of JavaScript Runtime

  1. The JavaScript Engine: The Code Executor
    The JavaScript engine is the heart of the JavaScript runtime. It runs on the main thread, executing the synchronous JavaScript code. The engine manages code execution using the execution context and memory heap. Every browser has its own JavaScript engine. For example, Chrome has a V8 engine, Firefox has Spidermonkey, and Safari has JavaScript Go.

  2. Web APIs: JavaScript’s Extended Toolbox

    Web APIs are built-in methods or functionalities that web browsers provide to perform long-running tasks on a separate thread. They handle tasks such as HTTP requests, DOM events, and timer-based functions in the browser.

    💡
    Did you know, that the console.log function is a part of Web APIs and not JavaScript language?
  3. Callback Queue: Scheduling the Tasks
    The Callback Queue is a data structure that tracks callbacks from tasks like HTTP requests, DOM events, or timer-based functions handled by Web APIs in the browser or libuv (a Node.js environment). It is given lower priority. Tasks from the Callback queue run only after the Microtask Queue is cleared.

  4. Microtask Queue: High-Priority Async Handling
    The Microtask Queue tracks resolved tasks from asynchronous code like Promises or Mutation Observer. A high-priority queue runs after the current stack is cleared but before any task in the Callback Queue.

  5. Event Loop: The Conductor of Asynchronous Harmony
    JavaScript achieves concurrency through the event loop, which coordinates between the main thread and other environment parts like the Web APIs or Node.js APIs. The event loop monitors the call stack and task queues, ensuring JavaScript runs smoothly. Once all synchronous code is done and the call stack is clear, it moves pending tasks from the microtask or callback queue to the call stack for execution, prioritizing microtasks first.

Step-by-Step Execution Process

Synchronous code always runs first. The JavaScript engine executes synchronous code line by line on the main thread, clearing the call stack. Once the synchronous code is complete (i.e. the call stack is empty), the Event Loop begins processing queued tasks. The Microtask Queue is given priority over the Callback Queue (aka Task Queue).

So, Order of Execution: Synchronous code → Microtask Queue → Callback Queue.

Let’s understand this further with an example.

console.log("Start");
setTimeout(() => console.log("Timeout"), 0);
const promise = new Promise((resolve, reject) => {
    console.log("Promise Body");
    resolve("Async Result");
});

function fetchData() {
    fetch("https://dummyjson.com/test")
        .then(() => console.log("Data Recieved!"))
        .catch(() => console.log("Error Fetching Data!"));

    return new Promise(resolve => {
        setTimeout(() => resolve("Longer Promise"), 2000);
    });
}
fetchData().then(console.log);

promise.then(console.log).then(() => console.log("Chained Callback"));
console.log("End");
  1. In the above code, as soon as the program starts a global execution context is created and pushed onto the JavaScript engine’s call stack. All synchronous code starts running on the main thread.

  2. console.log(“Start");

    • Synchronous code execution has two phases, the technical details of which are explained here. For now, let’s consider that a Global execution context is created in the JavaScript engine at this step.

    • Immediately prints “Start” in the console.

  3. setTimeout(() => console.log("Timeout"), 0)

    • The callback function of setTimeout containing console.log is handed off to a Web API (timer).

    • After the specified delay (0 ms), its callback is pushed into the Callback Queue.

    • 💡Note: Even with 0 ms, the event loop won’t process this callback until the call stack and microtasks queue is empty.

  4. const promise = new Promise((resolve, reject) => {…})

    • A promise is created and assigned to a constant variable “promise”.

    • console.log("Promise body"); prints “Promise body”. The promise body is a synchronous code and gets executed immediately.

    • resolve("Async Result"); resolves promise immediately. But the callback console.log will be queued later on Microtask Queue when it is attached with .then().

  5. function fetchData(){…} is declared in the global execution context.

     Global Execution Context:
     promise = <Promise> {...},
     fetchData = {
         fetch("https://dummyjson.com/test")
             .then(() => console.log("Data Recieved!"))
             .catch(() => console.log("Error Fetching Data!"));
    
         return new Promise(resolve => {
             setTimeout(() => resolve("Longer Promise"), 2000);
         });
     }
     ...
    
  6. fetchData().then(console.log);

    • fetchData() is called, a Promise is created which schedules a setTimeout timer with a callback for 2000ms and an HTTP call is made using Web APIs.

    • The execution context of fetchData() pops out once it returns the Promise, while the 2000s delay timer and the HTTPS call are still running on the browser’s separate thread.

    • The JavaScript engine does not wait for these operations to complete and continues executing synchronous code.

  7. promise.then(console.log).then(() => console.log("Chained Callback"));

    • The .then() to promise adds console.log("Async Result"); callback to the Microtask Queue.

    • 💡 Note: The second callback is chained so it will be enqueued to the Microtask Queue only after the first callback is executed.

  8. console.log("End"); prints “End” in the console immediately.

  9. Once the whole synchronous code is executed and the call stack is empty, the event loop pushes the microtasks or callbacks to the call stack based on priority.

  10. Since console.log("Async Result"); is the first task on the Microtask Queue, the Event loop pushes it to the call stack and “Async Result” is printed.

  11. The event loop pushes the chained callback () => console.log("Chained Callbak") to the Microtask Queue and then to the call stack, which prints “Chained callback”.

  12. Since the call stack and the microtask queue are empty, the event loop pushes callbacks from the Callback Queue to the call stack.

  13. () => console.log("Timeout") will be pushed to the call stack because it was requested before, as the delay was 0ms. Prints “Timeout” in the console. Also, let’s assume the HTTPS fetch call gets done here and the fetch function always returns a promise which gets queued in the Microtask Queue.

  14. The callback of the fetch function is executed, and it prints “Data Received!”.
    If the delay of 2000ms is still ongoing, the runtime will wait for the Web API to push () => resolve("Longer Promise") onto the Callback Queue after it’s done. If it is already done, the callback will be queued in the Callback Queue, and then the event loop will push the resolve function to the call stack.

  15. It will resolve the promise followed by .then() and print the resolved value “Long Promise” in the console.

Final Output:

Start
Promise body
End
Async Result
Chained Callback
Timeout
Data Received!
Long Promise

With this, we cover an in-depth JavaScript Runtime environment.

1
Subscribe to my newsletter

Read articles from Yugam Gangar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Yugam Gangar
Yugam Gangar

I am another engineer sharpening my technical writing skills and exploring my knowledge through this platform.