Free lesson · JavaScript Basic to Advance
Execution Context
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:
- The engine creates an execution context for
example. - During the creation phase, it scans the body. It finds
var aand registersaasundefined. It findsfunction greetand registers it with the full function definition. - The execution phase begins. The first
console.log(a)readsafrom the variable environment and getsundefined. The secondconsole.logreadsgreetand finds the function.
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:
- Just before
first()is called:[global] - Inside
first:[global, first] - Inside
second(called fromfirst):[global, first, second] - Inside
third(called fromsecond):[global, first, second, third] - When
thirdreturns:[global, first, second] - When
secondreturns:[global, first] - When
firstreturns:[global]
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:
makeCounter()is called. A new execution context is created. Its variable environment hascount(set to0) andincrement(the inner function).- The line
return incrementreturns the inner function to the caller. Normally, after returning,makeCounter's execution context would be cleaned up. - But the returned
incrementfunction holds a reference back tomakeCounter's variable environment — becauseincrementreadscountfrom there. So the engine cannot clean up that environment. It stays alive in memory. counter(which isincrement) is called. A new execution context is created forincrement. Inside, it readscountfrommakeCounter's preserved environment (the count is 0), increments it to 1, and returns 1.counter()is called again. The samecount(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.