AlgoInsight

Free lesson · JavaScript Basic to Advance

Execution Context

Easy5 min read

What Happens When JavaScript Runs Your Code

Every time JavaScript runs a function — or your script for the first time — it creates a structure called an execution context. The execution context is where the function's variables live, where its parameters are stored, and where the this keyword gets its value. Understanding execution contexts is the foundation for everything in Module 6 and beyond.

I'm Nikhil. I'll be your guide through all of Module 6. Closures are the single most-misunderstood concept in JavaScript, and getting them right requires understanding the runtime model underneath. We're going to spend this Topic building that model carefully, and the rest of the module applying it.

function outer() {
  const a = 1;

  function inner() {
    const b = 2;
    console.log(a + b);
  }

  inner();
}

outer();

That code creates three execution contexts. The global context (where outer is defined). The context for outer() when it's called (where a and inner live). The context for inner() when it's called (where b lives, and where a is still accessible from the outer context). Each context is a structure the JavaScript engine creates, uses, and eventually discards.

If this sounds abstract, stay with me. The execution context model is what makes closures, scope, and async all make sense. Without it, JavaScript's behavior seems random. With it, everything has a reason.

What an Execution Context Contains

When the JavaScript engine creates an execution context for a function call, it sets up three things:

1. A variable environment. Where all the variables and function declarations in this context live. When you write const x = 5 inside a function, x gets stored here.

2. A reference to the outer environment. The scope chain we covered in Module 3 — the "where do I look next if I can't find this variable here" pointer. This is what enables nested functions to see variables from their enclosing scopes.

3. A binding for this. What this refers to inside this context. The rules for this are complex and we cover them in Module 8. For now, just know that every context has its own this binding.

graph TD
    A[Execution Context] --> B[Variable Environment<br/>local variables, parameters,<br/>function declarations]
    A --> C[Outer Environment Reference<br/>pointer to enclosing context]
    A --> D[this binding<br/>set when context is created]

    style A fill:#7c3aed,color:#fff
This diagram shows the three components of an execution context. Each execution context has a variable environment where local variables, parameters, and function declarations are stored. It has a reference to the outer environment, which is a pointer to the enclosing execution context — this is how the scope chain is implemented. And it has a binding for the this keyword, which is set when the context is created based on how the function was called.

These three components are what give a function call its own "world." Variables you declare inside don't leak out. Variables from outside are still accessible through the outer reference. this is whatever it should be for that particular call.

The Two Phases

Every execution context goes through two phases:

Phase 1 — Creation. Before any code in the context runs, the engine scans the entire scope and registers all declarations. Function declarations are registered with their full definitions. var declarations are registered with undefined. let and const declarations are registered in an uninitialized state. The this binding is set. The outer environment reference is set.

Phase 2 — Execution. Now the engine runs the code line by line, top to bottom. Assignments store values into the slots prepared in Phase 1. Function calls create new execution contexts. Returns destroy the current context and resume the previous one.

This is the two-phase model we touched on in Topic 3.4 when we covered hoisting. Hoisting is literally just what happens during the creation phase. It's not magic. It's the engine doing setup work before running your code.

function example() {
  console.log(a);              // undefined (var was hoisted)
  console.log(typeof greet);   // "function" (declaration fully hoisted)

  var a = 1;

  function greet() {
    return "hi";
  }
}

example();

When example() is called:

Nothing is moving around in your source code. The behavior is determined by the order of the two phases.

Interactive animation: Phase 1 sets up, phase 2 runs - var, let and function declarations registered first, then the lines execute.

The Call Stack

The JavaScript engine keeps track of which execution context is currently running using a call stack. Each new function call pushes a new context onto the stack. Each return pops one off.

function third() {
  console.log("third runs");
  return 3;
}

function second() {
  console.log("second runs");
  const result = third();
  return result;
}

function first() {
  console.log("first runs");
  const result = second();
  return result;
}

console.log(first());

The call stack at various points:

The currently running context is always the one on top of the stack. When a function returns, its context is popped off, and execution resumes where the previous context left off.

This is also where stack overflow errors come from. Recursive functions that never reach a base case keep pushing contexts onto the stack until the engine runs out of memory:

function recurse() {
  recurse();   // never returns
}

// recurse();   // ❌ would throw: Maximum call stack size exceeded

The call stack is finite. Every modern JavaScript engine has a maximum depth, usually around 10,000 to 15,000 frames. Going past it throws a RangeError.

Interactive animation: A full run, in and out - each call pushes a context, each return pops it and the caller resumes.

Interactive animation: Recursion with no way down - contexts keep pushing until the stack runs out and a RangeError is thrown.

Why This Matters for Closures

Here's the bridge to the rest of Module 6: closures work because the outer environment reference outlives the execution context that created it.

When a function is created inside another function, the inner function gets a reference to the outer function's variable environment. Normally, when the outer function returns, its execution context is popped off the stack and its variables can be garbage collected.

But if the inner function still exists somewhere — stored in a variable, attached as an event handler, captured in another scope — it still has that reference. JavaScript can't garbage collect the outer function's variables, because the inner function might still need them.

That preserved access to a scope that should have been cleaned up is a closure. We'll formalize this in Topic 6.3. For now, internalize the mechanism: contexts get destroyed when functions return, unless something is still holding a reference to that context's variables.

A Concrete Trace

Let me trace through a small example to make this real.

function makeCounter() {
  let count = 0;

  function increment() {
    count = count + 1;
    return count;
  }

  return increment;
}

const counter = makeCounter();
console.log(counter());   // 1
console.log(counter());   // 2

The trace:

  1. makeCounter() is called. A new execution context is created. Its variable environment has count (set to 0) and increment (the inner function).
  2. The line return increment returns the inner function to the caller. Normally, after returning, makeCounter's execution context would be cleaned up.
  3. But the returned increment function holds a reference back to makeCounter's variable environment — because increment reads count from there. So the engine cannot clean up that environment. It stays alive in memory.
  4. counter (which is increment) is called. A new execution context is created for increment. Inside, it reads count from makeCounter's preserved environment (the count is 0), increments it to 1, and returns 1.
  5. counter() is called again. The same count (still alive in memory) is now 1. It gets incremented to 2 and returned.

The key insight: count should have been destroyed when makeCounter returned. But because increment held a reference to it, JavaScript kept it alive. That preservation is the entire foundation of closures.

Common Pitfalls

Warning: The execution context traps.

Thinking execution contexts are physical things in your code. They're runtime structures the engine creates. They don't exist while you're reading source code. They come into existence when your code runs.

Confusing the call stack with the scope chain. The call stack tracks which functions are currently running. The scope chain tracks which variables are accessible. They're related but separate. A function called from anywhere can still see its original lexical scope, regardless of the current call stack.

Forgetting that execution contexts are cleaned up. When a function returns, its context is popped off the stack. Variables are eligible for garbage collection — unless something outside still references them, which is exactly what closures rely on.

Why This Matters in Interviews

Interview: Senior JavaScript interviews probe the execution context model. Questions like "what happens when JavaScript runs a function?" or "how does the engine handle nested function calls?" expect answers in terms of contexts, the call stack, and variable environments.

The depth of your answer signals whether you've studied JavaScript at the spec level or just at the API level. Reciting "scope is where variables live" is API-level. Talking about variable environments, outer references, and the two-phase context creation is spec-level.

Topic 6.7 — Interview Lens: Closures — covers the deeper treatment of these questions, including the puzzles interviewers use to probe execution context understanding.

Try It

Exercise: Draw the call stack at each step of this code (on paper or in your head):

function a() {
  b();
  console.log("a finished");
}

function b() {
  c();
  console.log("b finished");
}

function c() {
  console.log("c running");
}

a();

What gets logged, and in what order? Trace the call stack at each console.log. Once you can do this in your head — when each function is called, when each one returns, what's on the stack at each moment — you've internalized the runtime model. Everything in this Module builds on this.

Back to JavaScript Basic to Advance