What happens when you run a JS code
Part 1: The Two Phases
Every time JavaScript runs your code, it goes through exactly two phases inside the JS engine. Understanding these phases explains hoisting, the temporal dead zone, and many other behaviours that seem like magic.
Phase 1: Memory Creation (Compilation)
Before a single line of your code runs, the engine scans through the entire script. It allocates memory for all variables and functions it finds. Variables get undefined, function declarations get the whole function body stored.
Phase 2: Code Execution
Now the engine goes line by line from top to bottom and actually runs the code — assigning values, calling functions, evaluating expressions.
Part 2: Step by Step — What Actually Happens
Step 1: Parsing — code becomes tokens
The engine reads your raw text and breaks it into meaningful pieces called tokens: keywords (let, function), identifiers (variable names), operators, literals.
// Your source code:
let x = 10 + 5;
// Becomes tokens:
// [let] [x] [=] [10] [+] [5] [;]Step 2: AST — tokens become a tree
The tokens are arranged into an Abstract Syntax Tree (AST) — a tree structure that represents the meaning of your code. Tools like linters, Babel, and TypeScript all work by reading the AST.
// let x = 10 + 5 becomes roughly:
//
// VariableDeclaration
// └── VariableDeclarator
// ├── Identifier (x)
// └── BinaryExpression (+)
// ├── Literal (10)
// └── Literal (5)Step 3: Compilation — AST becomes bytecode
Modern JS engines (like V8) don't just interpret the AST line by line — they compile it to bytecode first, then use a JIT (Just-In-Time) compiler to turn hot code paths into native machine code for maximum speed.
// You write:
function add(a, b) { return a + b; }
for (let i = 0; i < 1000000; i++) add(i, 1);
// After many calls, V8 notices add() is "hot"
// → JIT compiles it directly to machine code
// → runs at near-native speedStep 4: Execution Context is created
Every time code runs, an Execution Context is created. There is always one Global Execution Context for the whole script, and a new one is created each time a function is called. Each context has two things: a Variable Environment (where variables live) and a reference to the outer scope.
// Global Execution Context created first:
let name = "Arjun";
function greet() {
// New Execution Context created when greet() is called:
let msg = "Hello, " + name; // can access outer 'name'
console.log(msg);
}
greet(); // Hello, ArjunStep 5: Memory phase — hoisting happens here
// Before execution, memory phase scans the code:
console.log(x); // undefined (not an error!)
var x = 10; // ← x was found in memory phase, set to undefined
greet(); // ✅ works! function stored in memory phase
function greet() { console.log("Hi!"); }
console.log(y); // ❌ ReferenceError
let y = 5; // let is in TDZ until this linePart 3: The Full Picture
// Here's what happens when you run this script:
var a = 1; // Memory phase: a = undefined
// Execution phase: a = 1
function sayHi() {
var b = 2; // New execution context when called
console.log(a + b);
}
sayHi(); // → creates context → b=2 → logs 3
// → context destroyed after return
// Summary:
// 1. Parse → tokens → AST
// 2. Compile → bytecode (+ JIT for hot code)
// 3. Global execution context created
// 4. Memory phase: hoist vars (undefined) and functions
// 5. Execution phase: run line by line