RoadmapDay 1 / 80
JavaScript CoreMonth 1 · Week 1

Day 1: Execution Context, Call Stack & Hoisting Mechanics

Understand exactly what happens when the JS engine "runs" your code — from parsing to the creation and execution phases of every context — so hoisting stops feeling like magic.

Mark this day complete

Study

Concepts

What an Execution Context actually is

Every time the engine runs code, it wraps that code in an Execution Context: a container holding the variables, functions, and the value of `this` that are reachable from that piece of code. There are three kinds — the Global Execution Context (one per program), Function Execution Contexts (one per function call), and Eval contexts (rare, ignore for interviews).

Contexts are created in two distinct phases. The Creation (memory) phase scans the code without running it: it sets up the variable environment, hoists `var` declarations (initialized to `undefined`), hoists function declarations (fully, with their body attached), and creates — but does not initialize — `let`/`const` bindings. The Execution phase then runs line by line, assigning real values as it goes.

The Call Stack: LIFO execution order

The Call Stack is a Last-In-First-Out structure that tracks which execution context is currently active. Calling a function pushes a new Function Execution Context on top; returning from it pops that context off and resumes the one beneath.

Because JS has a single call stack, it is single-threaded for synchronous code — only one context runs at a time. A deeply recursive function without a base case keeps pushing frames until it exceeds the stack size, producing the classic "Maximum call stack size exceeded" RangeError.

Hoisting is a side effect of the Creation phase

`var` and function declarations are hoisted and initialized during Creation, which is why you can call a function before its declaration appears in the source. `let` and `const` are hoisted too, but left uninitialized in the "Temporal Dead Zone" (TDZ) until their declaration line executes — reading them earlier throws a ReferenceError rather than returning `undefined`.

Function *expressions* (`const f = function () {}`) are not hoisted as callable — only the variable binding is, following the `var`/`let` rule of whatever keyword declared it.

See It

Visualizations

Visualization

Two phases of every Execution Context

Creation runs first and sets up memory; Execution then runs the code top to bottom.

Parse

Engine scans the source for syntax errors

Creation Phase

Hoist var → undefined, hoist functions fully, reserve let/const in TDZ

Execution Phase

Run line by line, assign real values, invoke functions

Visualization

Call Stack while running greet()

Each call pushes a frame; each return pops it — LIFO order.

Call Stack

formatName()

topmost — currently executing

greet()

waiting for formatName() to return

Global Execution Context

created first, popped last

Build It

Code Examples

Hoisting: var vs let/const vs function

js
console.log(a);        // undefined  (var hoisted + initialized)
console.log(typeof sayHi); // "function" (fully hoisted)
// console.log(b);      // ReferenceError: Cannot access 'b' before initialization (TDZ)

var a = 10;
let b = 20;

function sayHi() {
  return 'hi';
}

// Function expressions only hoist the binding, not the value
console.log(typeof add); // "undefined"
var add = function (x, y) {
  return x + y;
};

Run this in a fresh scope. Notice the three different outcomes for the three declaration styles — that difference IS the Creation phase made visible.

Watching the Call Stack unwind

js
function formatName(name) {
  console.trace('formatName frame'); // prints the current stack
  return name.trim().toUpperCase();
}

function greet(name) {
  const formatted = formatName(name); // pushes formatName
  return `Hello, ${formatted}!`;      // formatName already popped here
}

console.log(greet('  ali  ')); // pushes greet -> formatName -> pops both

console.trace() prints the exact stack at that moment. Paste this into DevTools and step through with the debugger to see frames appear and disappear.

Remember

Key Takeaways

  • Execution = Creation phase (memory setup) + Execution phase (line-by-line run).
  • var is hoisted and initialized to undefined; let/const are hoisted but stuck in the TDZ.
  • Function declarations are hoisted whole; function expressions are not.
  • The Call Stack is LIFO — one frame executes at a time, synchronously.
  • Stack overflow = too many nested/recursive calls without a returning base case.

Do It

Practice

  1. 1Predict the console.log output of a snippet mixing var, let, const and a function declaration before running it — then verify in DevTools.
  2. 2Write a recursive function without a base case, run it, and read the RangeError stack trace to identify the repeating frame.
  3. 3Use the Sources panel debugger to step through a 3-level nested function call and watch the Call Stack pane.

Go Deeper

Resources

MDN: Execution contextECMA-262 §9 Executable Code and Execution Contexts