JavaScript Execution Context: A Complete Tutorial
Learn how JavaScript execution context works step by step. Covers the global execution context, function execution context, creation and execution phases, the call stack, variable environment, this binding, and how closures relate to execution context.
Execution context is the environment in which JavaScript code runs. Every time you run a script or call a function, JavaScript creates an execution context that manages the code's variables, scope, and this value. Understanding execution context explains why hoisting works, how the scope chain is built, why this changes depending on how you call a function, and how closures maintain access to outer variables.
What is an Execution Context?
An execution context is a container that holds:
- Variable Environment: all variables, function declarations, and arguments in this scope
- Scope Chain: a reference to the outer (parent) execution context
- this binding: the value of
thisin this context
JavaScript creates three types of execution contexts:
| Type | Created when | this value |
|---|---|---|
| Global Execution Context (GEC) | Script starts | window (browser) or globalThis |
| Function Execution Context (FEC) | A function is called | Depends on how the function is called |
| Eval Execution Context | eval() is called | Inherits from calling context |
The Global Execution Context
When JavaScript starts executing a script, it first creates the Global Execution Context:
// Global execution context is created automatically
var name = "Alice";
let age = 25;
function greet() {
console.log(`Hello, ${name}!`);
}
greet();The global execution context goes through two phases:
Phase 1: Creation Phase
During the creation phase, the engine:
- Creates the global object (
windowin browsers,globalin Node.js) - Creates the
thisbinding (points to the global object) - Sets up memory for all
vardeclarations (initialized toundefined) - Stores function declarations fully in memory
- Registers
let/constdeclarations (uninitialized - Temporal Dead Zone)
// After creation phase, before execution:
// name: undefined (var - hoisted and initialized)
// age: <uninitialized> (let - in TDZ)
// greet: function() { ... } (fully stored)
// this: window (browser) or globalThisPhase 2: Execution Phase
During the execution phase, the engine runs code line by line, assigning values and executing statements:
// Execution proceeds line by line:
// 1. name = "Alice" (assignment replaces undefined)
// 2. age = 25 (let exits TDZ, gets initialized)
// 3. greet() (function call -> new execution context)Function Execution Context
Every time a function is called, a new execution context is created for that function:
var globalVar = "global";
function outer() {
var outerVar = "outer";
function inner() {
var innerVar = "inner";
console.log(innerVar); // "inner" (own context)
console.log(outerVar); // "outer" (outer context via scope chain)
console.log(globalVar); // "global" (global context via scope chain)
}
inner();
}
outer();Function Context Creation
When outer() is called:
- New execution context created for
outer - Arguments object created with the function's parameters
- Variable Environment set up:
outerVar=undefined,inner= function - Scope Chain established:
outer.[[Scope]]= reference to global context thisbinding determined by howouterwas called
When inner() is called inside outer:
- New execution context created for
inner - Variable Environment:
innerVar=undefined - Scope Chain: inner -> outer -> global
thisbinding determined by the call
The Call Stack
The call stack is how JavaScript tracks which execution context is currently running. It follows Last In, First Out (LIFO) order:
function first() {
console.log("first start");
second();
console.log("first end");
}
function second() {
console.log("second start");
third();
console.log("second end");
}
function third() {
console.log("third");
}
first();Call Stack Progression
Step 1: [Global] // Script starts
Step 2: [Global, first()] // first() called
Step 3: [Global, first(), second()] // second() called inside first
Step 4: [Global, first(), second(), third()] // third() called inside second
Step 5: [Global, first(), second()] // third() returns, popped off
Step 6: [Global, first()] // second() returns, popped off
Step 7: [Global] // first() returns, popped off
Step 8: [] // Script ends, global popped
Output:
first start
second start
third
second end
first end
Visualizing with Stack Traces
When an error occurs, the stack trace shows the current state of the call stack:
function a() { b(); }
function b() { c(); }
function c() { throw new Error("Something went wrong"); }
a();
// Error: Something went wrong
// at c (script.js:3)
// at b (script.js:2)
// at a (script.js:1)
// at script.js:5Stack Overflow
The call stack has a finite size. Recursive functions that call themselves without a proper base case exhaust the stack:
function infinite() {
infinite(); // no base case
}
infinite();
// RangeError: Maximum call stack size exceededSingle-Threaded Execution
JavaScript has a single call stack, meaning it can only execute one piece of code at a time. This is why long-running synchronous operations block the UI. Asynchronous operations (setTimeout, fetch, Promises) use the event loop to avoid blocking the stack.
The Variable Environment in Detail
Each execution context has a Variable Environment that stores identifiers differently based on the declaration type:
function example(param1, param2) {
var varVariable = "var";
let letVariable = "let";
const constVariable = "const";
function innerFunc() {
return "inner";
}
}
example("a", "b");Variable Environment after creation phase:
Function Execution Context: example
├── Variable Environment:
│ ├── arguments: { 0: "a", 1: "b", length: 2 }
│ ├── param1: "a"
│ ├── param2: "b"
│ ├── varVariable: undefined (var - hoisted, initialized)
│ ├── letVariable: <uninitialized> (let - TDZ)
│ ├── constVariable: <uninitialized> (const - TDZ)
│ └── innerFunc: function() { ... } (fully hoisted)
├── Scope Chain: [example scope, global scope]
└── this: (depends on call method)
After execution phase:
├── Variable Environment:
│ ├── param1: "a"
│ ├── param2: "b"
│ ├── varVariable: "var"
│ ├── letVariable: "let"
│ ├── constVariable: "const"
│ └── innerFunc: function() { ... }
The this Binding
Each execution context determines its this value based on how the function was called:
// Global context: this = window (browser) or globalThis
console.log(this === window); // true (in browser)
// Function call: this = window (non-strict) or undefined (strict)
function standalone() {
console.log(this);
}
standalone(); // window (non-strict) or undefined (strict)
// Method call: this = the object
const user = {
name: "Alice",
greet() {
console.log(this.name);
},
};
user.greet(); // "Alice" (this = user)
// Constructor call: this = new object
function Person(name) {
this.name = name;
}
const p = new Person("Bob"); // this = new Person instance
// Explicit binding: this = specified object
function sayName() {
console.log(this.name);
}
sayName.call({ name: "Charlie" }); // "Charlie"Arrow Functions and this
Arrow functions do not create their own this binding. They inherit this from the enclosing execution context:
const team = {
name: "Developers",
members: ["Alice", "Bob"],
printMembers() {
// Regular function: 'this' from method call = team
this.members.forEach((member) => {
// Arrow function: inherits 'this' from printMembers context
console.log(`${member} is in ${this.name}`);
});
},
};
team.printMembers();
// "Alice is in Developers"
// "Bob is in Developers"Scope Chain and Execution Context
The scope chain is established during the creation phase of each execution context. It links to the Variable Environment of the outer (lexical parent) context:
const global = "G";
function outer() {
const outerVar = "O";
function middle() {
const middleVar = "M";
function inner() {
const innerVar = "I";
// Scope chain resolution:
console.log(innerVar); // found in inner's Variable Environment
console.log(middleVar); // found in middle's Variable Environment
console.log(outerVar); // found in outer's Variable Environment
console.log(global); // found in global Variable Environment
}
inner();
}
middle();
}
outer();Scope Chain Diagram
inner EC
├── Variable Environment: { innerVar: "I" }
├── Scope Chain: inner -> middle -> outer -> global
└── Lookup: innerVar ✓ -> middleVar ✓ -> outerVar ✓ -> global ✓
middle EC
├── Variable Environment: { middleVar: "M", inner: function }
├── Scope Chain: middle -> outer -> global
outer EC
├── Variable Environment: { outerVar: "O", middle: function }
├── Scope Chain: outer -> global
Global EC
├── Variable Environment: { global: "G", outer: function }
├── Scope Chain: global (end of chain)
Closures and Execution Context
A closure occurs when a returned function maintains a reference to its outer execution context's Variable Environment, even after the outer function has finished executing:
function createCounter() {
let count = 0; // stored in createCounter's Variable Environment
return function increment() {
count++;
return count;
};
}
const counter = createCounter();
// createCounter's execution context is popped from the stack
// BUT its Variable Environment is kept alive by the closure
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3What Happens Step by Step
createCounter()is called - new FEC createdcountis initialized to 0 in the Variable Environmentincrementfunction is created with a reference tocreateCounter's scopeincrementis returnedcreateCounter's FEC is popped from the call stack- But
createCounter's Variable Environment stays in memory (referenced byincrement) - Each call to
counter()creates a new FEC forincrement, which accessescountthrough its scope chain
Complete Example: Tracing Execution
var x = 10;
function foo() {
var y = 20;
function bar() {
var z = 30;
console.log(x + y + z);
}
bar();
}
foo();Full Trace
1. Global EC created
Variable Environment: { x: undefined, foo: function }
this: window
2. Execution starts
x = 10
3. foo() called - new FEC pushed
Call Stack: [Global, foo]
Variable Environment: { y: undefined, bar: function }
Scope Chain: foo -> global
this: window
4. y = 20
5. bar() called - new FEC pushed
Call Stack: [Global, foo, bar]
Variable Environment: { z: undefined }
Scope Chain: bar -> foo -> global
this: window
6. z = 30
7. console.log(x + y + z)
z found in bar's VE: 30
y found in foo's VE (via scope chain): 20
x found in global VE (via scope chain): 10
Output: 60
8. bar() returns - FEC popped
Call Stack: [Global, foo]
9. foo() returns - FEC popped
Call Stack: [Global]
10. Script ends - Global EC removed
Call Stack: []
The Event Loop and Execution Context
When asynchronous code runs, the event loop manages which code gets a new execution context and when:
console.log("1: Synchronous");
setTimeout(function timer() {
console.log("2: setTimeout callback");
}, 0);
Promise.resolve().then(function micro() {
console.log("3: Promise microtask");
});
console.log("4: Synchronous");
// Output:
// 1: Synchronous
// 4: Synchronous
// 3: Promise microtask
// 2: setTimeout callbackWhy this order?
- Synchronous code runs first in the current execution context
- Microtasks (Promises) run after the current context but before macrotasks
- Macrotasks (setTimeout) run after all microtasks are completed
Each callback (timer, micro) gets its own Function Execution Context when it runs.
Rune AI
Key Insights
- Two phases: creation (memory setup, hoisting) and execution (line-by-line code running)
- Three parts: Variable Environment, scope chain, and
thisbinding - Call stack: tracks nested function calls, LIFO order, finite size
- Scope chain: links each context to its lexical parent for variable resolution
- Closures: inner functions keep their outer context's Variable Environment alive
- this varies: global, method, constructor, and explicit calls each set
thisdifferently
Frequently Asked Questions
Is the execution context the same as scope?
How many execution contexts can exist at once?
Does each arrow function get its own execution context?
What happens to execution context when an error is thrown?
Conclusion
Every piece of JavaScript code runs inside an execution context. The global execution context is created when a script starts. Each function call creates a new function execution context. Every context has a Variable Environment (storing variables and functions), a scope chain (linking to parent contexts), and a this binding. The call stack tracks which context is active, following last in, first out order. Understanding execution context explains hoisting (creation phase), scope chains (Variable Environment references), and closures (preserved Variable Environments). Use const and let for predictable behavior, and recognize that this is determined by how a function is called, not where it is defined.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.