Execution Context in JavaScript
When you write JavaScript code, you do not just hand it directly to your computer. JavaScript first needs to set up an environment before running anything. That environment is called the Execution Context.
An execution context is a container that holds everything JavaScript needs to run a piece of code — the variables, functions, and the value of this. Every time code runs, it runs inside an execution context.
Think of it like a work desk. Before you start any task, you set up your desk — you lay out the papers, put out your tools, and arrange your reference materials. You do not just start writing. You prepare first. The execution context is that workspace JavaScript sets up before it runs your code.
There are two types of execution context in JavaScript:
- Global Execution Context — created once when your script first loads
- Function Execution Context — created every time a function is called
// When this script loads, JavaScript creates a Global Execution Context.
// It prepares the following before running a single line:
// - A global object (window in browsers)
// - The value of `this`
// - Memory space for variables and functions
var message = "Hello"; // stored in memory during creation phase
var count = 10;
function greet() {
// A new Function Execution Context is created when greet() is called
var name = "Alice";
console.log(message + ", " + name); // Hello, Alice
}
greet();
Every execution context goes through two phases: a creation phase and an execution phase. We will cover both in detail shortly — but just know that JavaScript always does some setup work before it actually runs your code.
The Global Execution Context (GEC) is the very first execution context created when your JavaScript file loads. It is created automatically — you do not write any code to make it happen.
When the GEC is created, JavaScript does two important things:
- It creates the global object — in a browser, that is window. In Node.js, it is global.
- It sets up this — at the global level, this points to the window object in a browser.
Think of it like the main stage of a theatre. Even before any actors come out, the stage is fully set. The lights are on, the backdrop is in place, and the props are arranged. No one is performing yet — but the environment is ready. That is the global execution context.
There is always exactly one global execution context per program. It exists for the entire lifetime of the script.
// In a browser environment:
console.log(this === window); // true — at global level, this IS window
var siteName = "SimplyJavaScript";
// Variables declared at the global level become properties of window
console.log(window.siteName); // "SimplyJavaScript"
// Functions declared at global level also become window properties
function sayHi() {
console.log("Hi from global!");
}
console.log(typeof window.sayHi); // "function"
Notice how var variables declared at the global level automatically become properties of the window object. This is one reason why many developers prefer let and const — they do not attach themselves to window.
var x = 10;
let y = 20;
const z = 30;
console.log(window.x); // 10 — var attaches to window
console.log(window.y); // undefined — let does NOT attach to window
console.log(window.z); // undefined — const does NOT attach to window
Every time you call a function, JavaScript creates a brand new Function Execution Context (FEC) just for that function call. It is not shared — each call gets its own private workspace.
Inside that function execution context, JavaScript sets up:
- A Variable Environment — memory for all the variables declared inside the function
- A reference to the outer environment — so the function can access variables from its parent scope
- The value of this — which depends on how the function was called
Think of each function call like an actor stepping onto a stage and getting their own spotlight. When the actor is done performing, the spotlight disappears. The function execution context is exactly that — it exists while the function runs and is destroyed when the function returns.
function add(a, b) {
// A new Function Execution Context is created for add()
// It has its own memory: a, b, and result
var result = a + b;
return result;
// When this line runs, the FEC for add() is destroyed
}
function multiply(a, b) {
// A separate FEC is created for multiply()
var result = a * b;
return result;
}
var sum = add(3, 4); // FEC created → runs → destroyed
var product = multiply(3, 4); // Another FEC created → runs → destroyed
console.log(sum); // 7
console.log(product); // 12
Each call to add() or multiply() creates a completely new execution context. Even if you call the same function twice, each call gets its own separate context with its own separate variables. That is why recursion works — each recursive call is its own isolated context.
function greet(name) {
var message = "Hello, " + name + "!";
console.log(message);
}
// Each call creates its own FEC — completely independent
greet("Alice"); // Hello, Alice!
greet("Bob"); // Hello, Bob!
greet("Carol"); // Hello, Carol!
Every execution context — whether global or function — goes through a creation phase before any code actually runs. This phase is where JavaScript scans through the code and sets everything up in memory.
During the creation phase, JavaScript does the following:
- Variables declared with var are found and set to undefined in memory
- Function declarations are fully stored in memory — the entire function body is available immediately
- The value of this is determined
- A reference to the outer scope (scope chain) is set up
This is the exact reason hoisting exists. Hoisting is not magic — it is simply the result of the creation phase. JavaScript physically scans your code before running it, finds all var declarations and function declarations, and registers them in memory. By the time the execution phase starts, they are already there.
Think of it like a chef preparing to cook. Before they turn on the stove, they gather all the ingredients, measure everything out, and put it on the counter. They are not cooking yet — just getting ready. That pre-cooking setup is the creation phase.
// This code runs without an error — because of the creation phase
console.log(score); // undefined (not an error — var was hoisted)
console.log(greet); // [Function: greet] (full function was hoisted)
var score = 100;
function greet() {
console.log("Hello!");
}
// During creation phase, JavaScript registered:
// score → undefined
// greet → function greet() { console.log("Hello!"); }
Notice that accessing score before the assignment gives undefined, not an error. That is because var score was registered in the creation phase, but its value (100) has not been assigned yet — that happens in the execution phase.
let and const are also hoisted during the creation phase, but they are placed in a Temporal Dead Zone (TDZ). Accessing them before their declaration throws a ReferenceError:
console.log(a); // undefined — var is hoisted, initialized to undefined
// console.log(b); // ReferenceError — let is in the Temporal Dead Zone
// console.log(c); // ReferenceError — const is in the Temporal Dead Zone
var a = 1;
let b = 2;
const c = 3;
Once the creation phase is done, JavaScript enters the execution phase. This is where it actually runs your code — line by line, from top to bottom.
In the execution phase:
- Variables declared with var get their actual assigned values (replacing the initial undefined)
- Expressions are evaluated
- Functions are called, which triggers the creation of new Function Execution Contexts
- Code executes in the order it appears
Going back to the chef analogy — the execution phase is when the chef actually starts cooking. All the ingredients are prepped and on the counter (creation phase is done). Now they combine them step by step, following the recipe in order. That is the execution phase.
// ---- CREATION PHASE ----
// JavaScript scans and sets up memory:
// userName → undefined
// age → undefined
// showInfo → [full function stored]
// ---- EXECUTION PHASE ----
// JavaScript now runs line by line:
var userName = "Alice"; // userName changes from undefined → "Alice"
var age = 28; // age changes from undefined → 28
function showInfo() {
console.log(userName + " is " + age + " years old.");
}
showInfo(); // "Alice is 28 years old."
// What if we call before assignment?
console.log(userName); // "Alice" — we're past the assignment line now
The key thing to remember is that the creation phase and the execution phase happen in sequence. JavaScript never skips one or runs them together. Creation always comes first, then execution.
// Demonstrating the two phases in action:
console.log(num); // undefined — creation phase set it to undefined
var num = 42;
console.log(num); // 42 — execution phase assigned the value
// Function declaration — fully available even before the code runs
sayHello(); // "Hello!" — works because of creation phase hoisting
function sayHello() {
console.log("Hello!");
}
// Function expression — only the variable is hoisted, not the function
// greet(); // TypeError: greet is not a function
var greet = function() {
console.log("Hi!");
};
JavaScript uses a call stack to manage execution contexts. Every time a new execution context is created, it is pushed onto the call stack. When a function finishes running, its execution context is popped off the stack.
The call stack follows a Last In, First Out (LIFO) order. The last execution context to be pushed onto the stack is the first one to be removed. The global execution context is always at the bottom — it is only removed when the entire program finishes.
Think of it like a pile of cafeteria trays. Each time a function is called, a new tray is placed on top of the pile. When the function finishes, its tray is removed. The bottom tray (the global one) stays there until the whole program is done.
function third() {
console.log("Inside third()");
// Call stack at this point:
// [ Global EC, first() EC, second() EC, third() EC ]
}
function second() {
console.log("Inside second()");
third(); // pushes third() EC onto the stack
// After third() returns, third() EC is popped off
console.log("Back in second()");
}
function first() {
console.log("Inside first()");
second(); // pushes second() EC onto the stack
// After second() returns, second() EC is popped off
console.log("Back in first()");
}
// Call stack starts with just: [ Global EC ]
first(); // pushes first() EC onto the stack
// Output order:
// Inside first()
// Inside second()
// Inside third()
// Back in second()
// Back in first()
The call stack is also why you see a stack overflow error in infinite recursion. If a function keeps calling itself without a base case, execution contexts keep getting pushed onto the stack until the browser runs out of memory:
// This will cause a stack overflow — do not run this in production!
function infinite() {
infinite(); // creates a new EC every time, never stops
}
// infinite(); // Uncaught RangeError: Maximum call stack size exceeded
Understanding the call stack helps you read error stack traces. When you see a list of function names in an error message, that is the call stack at the moment the error occurred — each line is an execution context that was active.
- Execution Context is the environment JavaScript sets up before running any code. It holds variables, functions, and the value of this.
- Global Execution Context (GEC) is created once when the script loads. It creates the global object (window in browsers) and sets this to point to it.
- Function Execution Context (FEC) is created every time a function is called. Each call gets its own isolated context with its own variables.
- Creation Phase happens before any code runs. JavaScript registers all var declarations as undefined and stores full function declarations in memory. This is why hoisting works.
- Execution Phase is when JavaScript actually runs the code line by line. Variables get their real values, functions are called, and execution contexts are created and destroyed.
- Call Stack manages execution contexts using LIFO order. Each new function call pushes an EC onto the stack. When the function returns, the EC is popped off.
- var declarations are hoisted and initialized to undefined during the creation phase. let and const are hoisted but remain in the Temporal Dead Zone until their declaration line is reached.
- The Global Execution Context stays on the call stack for the entire lifetime of the program. Function Execution Contexts are temporary — they are created and destroyed with each function call.
- What is an execution context in JavaScript?
- How many types of execution contexts are there, and what are they?
- What is the Global Execution Context and when is it created?
- What two things does JavaScript create when it sets up the Global Execution Context?
- What is the difference between the creation phase and the execution phase?
- Why does accessing a var variable before its declaration return undefined instead of throwing an error?
- What is hoisting, and how is it related to the creation phase of an execution context?
- Why do let and const throw a ReferenceError when accessed before their declaration, if they are also hoisted?
- What is the call stack, and how does it relate to execution contexts?
- What happens to a Function Execution Context after the function returns?
- What causes a "Maximum call stack size exceeded" error?
- How does the value of this differ between the Global Execution Context and a Function Execution Context?