JS Execution Context Deep Dive: Full Tutorial
Understand the JavaScript execution context in depth. Learn the creation and execution phases, variable environment vs lexical environment, the this binding, and how contexts stack on the call stack.
Every piece of JavaScript code runs inside an execution context. An execution context is the environment that JavaScript creates to run a specific piece of code. It contains the lexical environment (variable bindings), the this binding, and the reference to the outer scope. Understanding execution contexts explains why hoisting exists, how closures work, and what the call stack actually manages.
Types of Execution Contexts
JavaScript creates three types of execution contexts:
| Type | Created When | Contains |
|---|---|---|
| Global Execution Context | Script starts | Global variables, functions, this = globalThis |
| Function Execution Context | A function is called | Local variables, parameters, this = depends on call |
| Eval Execution Context | eval() is called | Variables declared inside eval (avoid using eval) |
// --- Global Execution Context starts here ---
const appName = "MyApp";
function initialize() {
// --- Function Execution Context for initialize() ---
const config = { debug: true };
function loadModules() {
// --- Function Execution Context for loadModules() ---
console.log(`Loading modules for ${appName}`);
}
loadModules();
}
initialize();The Two Phases
Every execution context goes through two phases: creation and execution.
Phase 1: Creation Phase
Before any code in the scope runs, the engine:
- Creates the lexical environment with all variable bindings
- Sets the
thisbinding - Sets the outer environment reference
function demo() {
// CREATION PHASE scans this function and builds:
// LexicalEnvironment: {
// multiply: <function reference> -- function declaration, fully initialized
// result: <uninitialized> -- let, in temporal dead zone
// }
// VariableEnvironment: {
// count: undefined -- var, initialized to undefined
// }
// ThisBinding: depends on how demo() was called
console.log(count); // undefined (var is hoisted with value undefined)
console.log(multiply); // [Function: multiply] (fully hoisted)
// console.log(result); // ReferenceError (let is in TDZ)
var count = 5;
let result = count * 2;
function multiply(a, b) {
return a * b;
}
}Phase 2: Execution Phase
Code runs line by line. Variables are assigned their actual values:
function demo() {
// --- Execution Phase ---
// Line 1: count is already undefined from creation phase
var count = 5;
// Now: count = 5
let result = count * 2;
// Now: result = 10 (TDZ is over for result)
function multiply(a, b) {
return a * b;
}
// multiply was already available from creation phase
console.log(count); // 5
console.log(result); // 10
console.log(multiply(3, 4)); // 12
}
demo();Phase Timeline
| Phase | var Variables | let/const | Function Declarations | this |
|---|---|---|---|---|
| Creation | Set to undefined | Exist but uninitialized (TDZ) | Fully initialized | Bound |
| Execution | Assigned actual value | Assigned when statement runs | Already available | Already bound |
Lexical Environment vs Variable Environment
Each execution context has two environment components:
- LexicalEnvironment: Stores
let,const, function declarations, and block-scoped bindings - VariableEnvironment: Stores
vardeclarations
They start as the same environment but diverge when blocks are entered:
function example() {
// Both LexicalEnv and VariableEnv point to the function environment
// VariableEnv: { x: undefined }
// LexicalEnv: { y: <uninitialized> }
var x = 1;
let y = 2;
if (true) {
// A new LexicalEnv is created for this block
// New LexicalEnv: { z: <uninitialized>, w: <uninitialized> }
// VariableEnv: still points to function env { x: 1, v: undefined }
let z = 3;
const w = 4;
var v = 5; // var goes to VariableEnv (function level)
console.log(x, y, z, w, v); // 1 2 3 4 5
}
// Back to function-level LexicalEnv
console.log(x, y, v); // 1 2 5
// console.log(z); // ReferenceError
}The this Binding
The this value is determined during the creation phase based on how the function is called:
// Global context: this = globalThis (window in browsers)
console.log(this === globalThis); // true (in non-strict mode)
const obj = {
name: "Alice",
greet() {
// Method call: this = obj (the object before the dot)
console.log(this.name); // "Alice"
},
greetArrow: () => {
// Arrow function: this = outer this (global)
console.log(this.name); // undefined
}
};
obj.greet(); // "Alice"
obj.greetArrow(); // undefined
function standalone() {
// Regular function call: this = globalThis (or undefined in strict mode)
console.log(this);
}
standalone(); // globalThis or undefinedthis Binding Rules Summary
| Call Pattern | this Value | Example |
|---|---|---|
| Method call | The object | obj.method() -> this === obj |
| Regular call | globalThis (sloppy) / undefined (strict) | fn() |
new call | New empty object | new Fn() -> this === {} |
call/apply/bind | Explicitly set | fn.call(obj) -> this === obj |
| Arrow function | Inherited from lexical scope | No own this |
Execution Context Lifecycle
const name = "Global";
function first() {
const name = "First";
function second() {
const name = "Second";
console.log(name); // "Second"
}
second();
console.log(name); // "First"
}
first();
console.log(name); // "Global"Step-by-step lifecycle:
1. Engine starts
-> Create Global Execution Context
-> Push to call stack
-> Creation phase: name = <uninitialized>, first = <function>
-> Execution phase: name = "Global"
2. first() is called
-> Create Function Execution Context for first()
-> Push to call stack [Global, first]
-> Creation phase: name = <uninitialized>, second = <function>
-> Execution phase: name = "First"
3. second() is called
-> Create Function Execution Context for second()
-> Push to call stack [Global, first, second]
-> Creation phase: name = <uninitialized>
-> Execution phase: name = "Second", log "Second"
4. second() finishes
-> Pop second from call stack [Global, first]
-> second's execution context is destroyed
5. first() continues
-> log "First"
-> first() finishes
-> Pop first from call stack [Global]
6. Global continues
-> log "Global"
How Closures Work Through Execution Contexts
When a function creates and returns an inner function, the inner function's [[Environment]] holds a reference to the outer function's lexical environment. Even though the outer execution context is popped from the stack, its lexical environment is not garbage collected:
function makeCounter() {
// EC for makeCounter:
// LexicalEnv: { count: 0 }
// Outer: Global LexicalEnv
let count = 0;
return function increment() {
// increment.[[Environment]] = makeCounter's LexicalEnv
count++;
return count;
};
}
const counter = makeCounter();
// makeCounter's EC is popped from the stack
// But makeCounter's LexicalEnv is NOT destroyed
// because counter (increment) holds a reference to it
counter(); // 1 -- creates new EC for increment, resolves count via [[Environment]]
counter(); // 2 -- new EC, same [[Environment]], count is now 2Global Execution Context in Detail
The global execution context is special:
// Global Execution Context:
// {
// LexicalEnvironment: {
// EnvironmentRecord: {
// // Object Environment Record (var + function declarations)
// ObjectRecord: { /* bound to globalThis */ },
// // Declarative Environment Record (let + const)
// DeclarativeRecord: { /* NOT on globalThis */ }
// },
// OuterEnv: null
// },
// VariableEnvironment: {
// // Points to the Object Environment Record
// },
// ThisBinding: globalThis
// }
var a = 1; // -> ObjectRecord -> window.a = 1
let b = 2; // -> DeclarativeRecord (NOT on window)
function c() {} // -> ObjectRecord -> window.c = c
console.log(window.a); // 1
console.log(window.b); // undefined (b is not on window, let uses DeclarativeRecord)
console.log(window.c); // [Function: c]Nested Execution Contexts Example
function outer(x) {
function middle(y) {
function inner(z) {
return x + y + z;
}
return inner(3);
}
return middle(2);
}
const result = outer(1); // 6Each function call creates its own execution context with its own lexical environment:
| Call Stack | Active EC | LexicalEnv | Outer Reference |
|---|---|---|---|
outer(1) | outer EC | { x: 1, middle: <fn> } | Global |
middle(2) | middle EC | { y: 2, inner: <fn> } | outer's LexicalEnv |
inner(3) | inner EC | { z: 3 } | middle's LexicalEnv |
Resolving x + y + z in inner:
zfound in inner's env -> 3yfound in middle's env (via outer ref) -> 2xfound in outer's env (via outer ref chain) -> 1- Result: 6
Strict Mode Effect on Execution Context
"use strict";
function strictDemo() {
// In strict mode:
// - this is undefined for regular function calls (not globalThis)
// - Assigning to undeclared variables throws ReferenceError
// - eval gets its own execution context (cannot modify calling context)
console.log(this); // undefined (NOT globalThis)
// x = 10; // ReferenceError: x is not defined (no implicit global)
}
strictDemo();Rune AI
Key Insights
- Three types of execution contexts: Global (one per script), function (one per function call), and eval (one per eval call), each with its own lexical environment and this binding
- Two phases per context: The creation phase scans declarations and sets up bindings (
varasundefined,let/constin TDZ, functions fully initialized), then the execution phase runs code line by line - LexicalEnvironment vs VariableEnvironment:
let/constgo in the LexicalEnvironment (block-scoped),vargoes in the VariableEnvironment (function-scoped), and they diverge when blocks are entered thisis determined by call pattern: Method calls bind to the object,newbinds to a fresh object,call/apply/bindset it explicitly, and arrow functions inheritthislexically- Closures persist lexical environments: When a function is returned, the execution context is destroyed but the lexical environment stays alive as long as any function references it through
[[Environment]]
Frequently Asked Questions
What triggers the creation of a new execution context?
What is the difference between execution context and scope?
What happens to an execution context after the function returns?
Why does var hoisting give undefined instead of an error?
How does the execution context determine the value of this?
Conclusion
Every JavaScript execution creates an execution context with two phases: creation (setting up variable bindings, this, and the outer scope reference) and execution (running code line by line). The lexical environment stores let/const bindings while the variable environment stores var bindings. Closures exist because a returned function's [[Environment]] keeps its parent's lexical environment alive even after the parent's execution context is destroyed.
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.