Execution Context in JavaScript

Why should you care about Execution Context in JavaScript?

Execution context explains hoisting, scope behavior, and why function calls run in a specific order. If these concepts confuse you today, this topic is what connects everything together.

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 the environment in which JavaScript code is evaluated and executed. It provides the bindings, function declarations, this value, and access to the outer environment that the code needs. Every time code runs, it runs within an execution context.

The two execution-context types you will use most often are:

  • Global Execution Context - created for the script's realm
  • Function Execution Context - created every time a function is called

javascript

// 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 endpoint = "/api/users";
  console.log(message + " " + endpoint); // Hello /api/users
}

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 sets up the global environment and global object for that realm:

  • It creates the global object - in a browser, that is window. In Node.js, it is global.
  • It sets up this according to the host and script type. In a browser classic script, top-level this is window; in an ES module, top-level this is undefined.

A realm has one global execution context for its global code. Its global bindings can remain available while the realm is alive, but the host may run separate jobs or scripts at different times.

javascript

// 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.

javascript

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

The function execution context is active while the function runs. After the function returns, it is no longer active, although a closure can keep some referenced bindings available.

javascript

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 -> returns
var product = multiply(3, 4); // Another FEC created -> runs -> returns

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. When a function returns, its context is no longer active; values captured by a closure can remain available afterward. That is also why recursion works - each recursive call has its own context.

javascript

function greet(name) {
  var message = "Hello, " + name + "!";
  console.log(message);
}

// Each call creates its own FEC - completely independent
greet("Order #101"); // Hello, Order #101!
greet("Order #102"); // Hello, Order #102!
greet("Order #103"); // Hello, Order #103!

For a regular function, this is usually decided by how the function is called, not where it was written. Ask about the call site first. Arrow functions are the important exception: they do not create their own this and capture it from the surrounding scope.

Rule table for the most common JavaScript this call forms.
Call form Value of this Example
object.method() The object before the dot. user.getName() → user
fn.call(value), apply, or bind The explicitly supplied value. show.call(user) → user
new Constructor() The new instance being created. new User() → the new user object
Plain regular call: fn() undefined in strict mode; the global object in sloppy mode. "use strict"; fn() → undefined
Arrow function: () => {} The surrounding this; call, apply, and bind cannot replace it. A callback inside user.show() keeps user

Arrow functions are useful for callbacks because they do not have their own this. They use the this value from their surrounding lexical environment. A regular callback gets its own call-site rule, so it can lose the object unless you bind it.

javascript

const user = {
  name: "Mina",
  showLater() {
    setTimeout(() => {
      console.log(this.name); // Mina: arrow keeps showLater's this
    }, 0);
  },
  getName() {
    return this.name;
  }
};

user.showLater();
console.log(user.getName()); // Mina

const detached = user.getName;
console.log(detached()); // undefined in strict mode: no object before the call
Important: do not use an arrow function as an object method when you need that method's own this. Use method syntax or a regular function for the method, then use an arrow inside it when you want to preserve the method's receiver.

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 setup helps explain behaviors commonly described as hoisting. Hoisting is not a separate runtime step; it is a useful way to describe how declarations can be available before their position in the source code.

Hoisting

Before executing code in an execution context, JavaScript establishes the relevant environments and bindings. var bindings are initialized to undefined, function declarations are available before their position in the source code, and let and const bindings remain in the Temporal Dead Zone until initialized.

javascript

// 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!");
}

// Conceptual model during setup:
//   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:

javascript

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;

My story: Hoisting confused me for months when I was starting out. I had a function that called another function defined 50 lines below, and it worked perfectly. Then I tried the same thing with a variable and got undefined instead of an error. I kept thinking JavaScript was reading my file out of order. Learning the creation phase gave me a useful model for why declarations can be available before their source-code position. Once I understood that, hoisting stopped being a mystery and became a predictable result of how environments and bindings are established.

Once the creation phase is done, JavaScript enters the execution phase. During execution, JavaScript evaluates statements and expressions according to the language's execution rules. For ordinary synchronous code, statements generally execute in source order.

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

javascript

// ---- CREATION PHASE ----
// JavaScript scans and sets up memory:
//   appName  -> undefined
//   version  -> undefined
//   showInfo -> [full function stored]

// ---- EXECUTION PHASE ----
// JavaScript now evaluates the code according to its execution rules:

var appName = "TaskBoard"; // appName changes from undefined -> "TaskBoard"
var version = 3;           // version changes from undefined -> 3

function showInfo() {
  console.log(appName + " is at version " + version + ".");
}

showInfo(); // "TaskBoard is at version 3."

// What if we call before assignment?
console.log(appName); // "TaskBoard" - 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.

javascript

// 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!");
};

The call stack tracks active execution contexts while JavaScript executes synchronous code. When a function call starts, its active call is pushed onto the stack. When the function returns, that call is popped from the stack.

The call stack follows a Last In, First Out (LIFO) order. The last call pushed onto the stack is the first call removed. In a simple script example, the global context is shown at the bottom while the script's top-level code is running. This is a useful model of the current synchronous call, not a claim about a one-to-one mapping to physical engine stack frames.

javascript

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 infinite recursion causes a stack overflow error. Each recursive call adds another active call to the call stack. Without a base case, the stack eventually exceeds its implementation limit, causing a RangeError: Maximum call stack size exceeded:

javascript

// 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 for global code. Its global object is window in a browser, and top-level this depends on whether the code is a classic script or an ES module.
  • 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 is the conceptual setup before code executes. var bindings are initialized to undefined, function declarations are available, and let/const bindings remain in the Temporal Dead Zone until initialized.
  • Hoisting is a useful term for explaining why some declarations are available before their position in the source code. It does not mean the engine performs a simple literal source scan.
  • Execution Phase is when JavaScript evaluates statements and expressions according to its execution rules. Ordinary synchronous statements generally execute in source order, values are assigned, and functions are called.
  • Call Stack tracks active synchronous calls using LIFO order. A function call is pushed when it starts and popped when it returns.
  • this is determined by the call form for regular functions: method, explicit call/apply/bind, new, or plain call. Arrow functions do not bind their own this; they use the value from their surrounding lexical environment.
  • 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.
  • Function Execution Contexts are temporary: they are created for function calls and are no longer active after those calls return. A closure can keep referenced bindings available afterward.
What's next? Head back to the Introduction tutorial to review the foundations, or explore other advanced topics in the sidebar.

Videos for this topic will be added soon.

Reviewed by

SimplyJavaScript Editorial Team

Technical editors and JavaScript educators with hands-on experience building frontend projects, writing learning material, and reviewing tutorials for clarity, accuracy, and beginner-friendly guidance.