How JavaScript Executes Code
JavaScript Engine
JavaScript runs inside an engine (V8 in Chrome/Node, SpiderMonkey in Firefox). The engine goes through these steps:
- Parsing — Source code is parsed into an Abstract Syntax Tree (AST).
- Compilation — The AST is compiled to bytecode by Ignition (and later optimized to machine code by TurboFan via JIT).
- Execution — The bytecode is executed line by line.
Execution Context
Every time JavaScript runs code, it creates an Execution Context. There are three types:
| Type | Created When |
|---|---|
| Global Execution Context | Script first loads |
| Function Execution Context | A function is invoked |
| Eval Execution Context | eval() is called |
Each execution context has two phases:
1. Creation Phase (Memory Allocation)
- Variables declared with
varare hoisted and set toundefined. - Variables declared with
let/constare hoisted but stay in the Temporal Dead Zone. - Function declarations are hoisted with their full body.
thisbinding 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:
| Panel | What it shows during a breakpoint |
|---|---|
| Threads | Shows which execution thread is paused (usually just "Main" for normal JS, but also shows Web Workers if you use them). |
| Watch | Lets 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. |
| Breakpoints | A list of all the manual breakpoints (blue tags) you've clicked on your code lines. You can check/uncheck them to enable/disable them. |
| Scope | Extremely important! Shows the current state of variables in the Local (current function), Closure (outer functions), and Global scopes. |
| Call Stack | Shows 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 Breakpoints | Lets you pause code execution exactly when a network request (AJAX/fetch) is sent or when its URL contains a specific string. |
| DOM Breakpoints | Pauses execution when a specific HTML element is modified, deleted, or its attributes change. |
| Global Listeners | Shows all event listeners (like 'click', 'keydown') bound to the window or document. |
| Event Listener Breakpoints | Pauses 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:
- Memory Creation Phase: Allocates memory for global variables (
windoworglobalobject) and hoists function declarations. - 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.
- Call Stack: Executes sync code (
1then4). - Microtask Queue: Promises have higher priority than
setTimeout.3runs immediately after sync code. - Macrotask Queue:
setTimeoutcallback (2) runs last.
Q: Can you explain the difference between Ignition and TurboFan in V8? A: V8 has two compilers:
- Ignition is the interpreter. It quickly generates unoptimized bytecode so the JS can start running instantly.
- 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.