Skip to main content

How JavaScript Executes Code

JavaScript Engine

JavaScript runs inside an engine (V8 in Chrome/Node, SpiderMonkey in Firefox). The engine goes through these steps:

  1. Parsing — Source code is parsed into an Abstract Syntax Tree (AST).
  2. Compilation — The AST is compiled to bytecode by Ignition (and later optimized to machine code by TurboFan via JIT).
  3. Execution — The bytecode is executed line by line.

Execution Context

Every time JavaScript runs code, it creates an Execution Context. There are three types:

TypeCreated When
Global Execution ContextScript first loads
Function Execution ContextA function is invoked
Eval Execution Contexteval() is called

Each execution context has two phases:

1. Creation Phase (Memory Allocation)

  • Variables declared with var are hoisted and set to undefined.
  • Variables declared with let/const are hoisted but stay in the Temporal Dead Zone.
  • Function declarations are hoisted with their full body.
  • this binding is determined.

2. Execution Phase

  • Code is executed line by line.
  • Variables are assigned their actual values.
  • Functions are invoked.

Call Stack

The call stack is a LIFO (Last In, First Out) data structure that tracks execution contexts.

function first() {
console.log("first");
second();
}

function second() {
console.log("second");
third();
}

function third() {
console.log("third");
}

first();

// Call Stack progression:
// 1. [Global]
// 2. [Global, first()]
// 3. [Global, first(), second()]
// 4. [Global, first(), second(), third()]
// 5. [Global, first(), second()] — third() pops off
// 6. [Global, first()] — second() pops off
// 7. [Global] — first() pops off

Stack Overflow

When the call stack exceeds its limit (e.g., infinite recursion):

function recurse() {
recurse(); // no base case
}
recurse(); // RangeError: Maximum call stack size exceeded

Chrome DevTools Debugging Panels

When debugging JavaScript in Chrome's Sources tab, you'll see several collapsible panels. Here's what they do:

PanelWhat it shows during a breakpoint
ThreadsShows which execution thread is paused (usually just "Main" for normal JS, but also shows Web Workers if you use them).
WatchLets you type in custom variable names or expressions (like user.name or count > 5) so you can watch their values change as you step through code.
BreakpointsA list of all the manual breakpoints (blue tags) you've clicked on your code lines. You can check/uncheck them to enable/disable them.
ScopeExtremely important! Shows the current state of variables in the Local (current function), Closure (outer functions), and Global scopes.
Call StackShows the exact chain of function calls (execution contexts) that led to the current line of code. Great for answering "how did I get here?".
XHR/fetch BreakpointsLets you pause code execution exactly when a network request (AJAX/fetch) is sent or when its URL contains a specific string.
DOM BreakpointsPauses execution when a specific HTML element is modified, deleted, or its attributes change.
Global ListenersShows all event listeners (like 'click', 'keydown') bound to the window or document.
Event Listener BreakpointsPauses code execution automatically when a specific event fires (e.g., pause whenever any "click" event happens).

Tip: The checkbox "Pause on caught/uncaught exceptions" acts like a global net. If your code throws an error, DevTools will automatically pause on that exact line before the app crashes.

Key Takeaways

  • JavaScript is single-threaded — one call stack, one thing at a time.
  • The engine creates an execution context for every function call.
  • The call stack manages the order of execution.
  • Stack overflow happens when the stack grows beyond its limit.

JIT Optimization Proof

Run this in Node.js — it proves that the same function gets faster on repeated calls because TurboFan compiles hot code to optimized machine code:

function heavyWork(n) {
let sum = 0;
for (let i = 0; i < n; i++) {
sum += Math.sqrt(i) * Math.sin(i);
}
return sum;
}

const iterations = 100000;
const rounds = 10;

console.log("Round | Time (ms) | Status");
console.log("------|-----------|-------");

for (let round = 1; round <= rounds; round++) {
const start = performance.now();
heavyWork(iterations);
const end = performance.now();
const time = (end - start).toFixed(3);

let status = "";
if (round === 1) status = "← Ignition (bytecode, cold)";
else if (round <= 3) status = "← TurboFan watching...";
else status = "← TurboFan optimized (machine code)";

console.log(` ${round} | ${time.padStart(9)} | ${status}`);
}

Expected output:

Round | Time (ms) | Status
------|-----------|-------
1 | 2.112 | ← Ignition (bytecode, cold)
2 | 1.227 | ← TurboFan watching...
3 | 1.220 | ← TurboFan watching...
4 | 1.382 | ← TurboFan optimized (machine code)
...
10 | 1.132 | ← TurboFan optimized (machine code)

Round 1 is the slowest (~2ms). By round 4+, TurboFan has optimized the hot function to machine code, making it ~47% faster (~1.1ms). This is JIT in action.

Interview Questions

Easy Level

Q: Explain what happens when a JavaScript script first runs.

A: JavaScript creates the Global Execution Context (GEC) and pushes it onto the Call Stack. It does this in two phases:

  1. Memory Creation Phase: Allocates memory for global variables (window or global object) and hoists function declarations.
  2. Code Execution Phase: Executes the code line by line.

Q: What is the output of this code, and what is the maximum call stack depth?

function a() { b(); }
function b() { c(); }
function c() { console.log("Done!"); }
a();

A: Output is "Done!". Maximum stack depth is 4 ([Global] -> a() -> b() -> c()).

Medium Level

Q: Will this code cause a Stack Overflow?

function loop() {
setTimeout(loop, 0);
}
loop();

A: No. setTimeout sends the callback to the Web API and Task Queue, not directly to the Call Stack. The loop function finishes and pops off the stack before the next one runs. The Event Loop safely handles it without piling up frames.

Q: Why does this code print undefined instead of throwing an error?

console.log(myVar);
var myVar = "Hello";

A: Because of the Creation Phase of the Execution Context. Before executing code, the engine scans for var declarations, allocates memory for myVar, and initializes it to undefined. This is called hoisting.

Hard Level

Q: What is the output order and why?

console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);

A: 1, 4, 3, 2.

  1. Call Stack: Executes sync code (1 then 4).
  2. Microtask Queue: Promises have higher priority than setTimeout. 3 runs immediately after sync code.
  3. Macrotask Queue: setTimeout callback (2) runs last.

Q: Can you explain the difference between Ignition and TurboFan in V8? A: V8 has two compilers:

  1. Ignition is the interpreter. It quickly generates unoptimized bytecode so the JS can start running instantly.
  2. TurboFan is the optimizing compiler. It watches for "hot" (frequently run) functions and recompiles them into highly optimized machine code (JIT compilation). If assumptions change (e.g., you pass a string instead of a number), TurboFan "de-optimizes" back to bytecode.