How JavaScript Really Executes Code: Execution Context and Scope Chain Explained

javascript dev.to

Note: This post is a translated version of an article originally published on my personal blog. You can read the original Korean post here.

Heads up: This post is written based on the ES5 specification. In ES6+, some details changed — such as where ThisBinding lives, and the distinct roles of VariableEnvironment vs LexicalEnvironment. I'll cover the ES6 version in a follow-up post.


Why Should You Care About Execution Context?

If you've been writing JavaScript for a while, you've probably run into these puzzling moments:

  • The same this keyword points to completely different objects depending on the situation
  • A variable you never declared somehow doesn't throw an error
  • You can freely access a variable that's defined outside your current scope

Sure, you can get by without knowing why these things happen. But eventually you hit a wall:

  • Your code "just works" but you can't explain why
  • You can't control what this points to
  • Variables get shared in unintended ways, spawning bugs everywhere

Understanding execution context is the key to breaking through that wall.


What Is an Execution Context?

An execution context is an object that holds all the environmental information needed to execute a piece of code.

JavaScript might look like it runs line by line, but under the hood, the engine first collects and prepares environmental information before executing anything. That prepared environment is the execution context.

When an execution context is activated, it gathers three pieces of information:

  • VariableEnvironment (VE) — a snapshot of the initial environment
  • LexicalEnvironment (LE) — the live environment that reflects changes during execution
  • ThisBinding — what this refers to in the current context

When Are Execution Contexts Created?

Execution contexts are created at two points:

  • When JavaScript first runs → the Global Execution Context is created
  • When a function is called → a Function Execution Context is created

The global execution context is born when the script starts and lives until the script ends.
A function execution context is created fresh every time a function is called, and it's discarded once the function finishes.


The Call Stack and Execution Contexts

Execution contexts are managed through the Call Stack.

function second() {
  console.log('second');
}

function first() {
  second();
}

first();
Enter fullscreen mode Exit fullscreen mode

Here's how this plays out step by step:

  1. The global execution context is pushed onto the call stack
  2. first() is called → its execution context is pushed on top
  3. second() is called → its execution context is pushed on top
  4. second finishes → its context is popped off
  5. first finishes → its context is popped off
  6. The global context is removed when the script ends

The context at the top of the stack is the one currently running. When a new function is called, the current context pauses and the new one takes over.


VariableEnvironment vs. LexicalEnvironment

Both environments start out identical when an execution context is created. Over time, they diverge:

  • LexicalEnvironment — updated in real time as code executes
  • VariableEnvironment — frozen at the state it was in when the context was first created

LexicalEnvironment

The LexicalEnvironment has two components.

environmentRecord

This is where identifier information for the current execution context is stored — parameter names, function declarations, and variable names all live here.

Before executing any code, the JavaScript engine scans through the scope and builds up this environmentRecord. It collects all the variable names ahead of time.

This is what we call hoisting.

For a deeper look at hoisting, check out my hoisting post.

The global execution context works slightly differently: its environmentRecord is the global object (window in browsers, global in Node.js). This is exactly why variables declared with var at the top level are accessible via window.yourVariable.

outerEnvironmentReference

This is a reference to the LexicalEnvironment of the scope in which the current function was declared.

The word "declared" is key here. It doesn't matter where the function is called — only where it was written. This is what makes JavaScript lexically scoped, and it's also the core principle behind closures.

Thanks to outerEnvironmentReference, when the engine needs to look up a variable, it can start in the current scope and walk outward.


The Scope Chain

How Variables Are Looked Up

When the JavaScript engine needs to resolve a variable, it searches in this order:

  1. Look in the current context's environmentRecord
  2. If not found, follow outerEnvironmentReference to the outer scope
  3. Repeat this process all the way up to the global scope
  4. If still not found → ReferenceError

This chain of scopes — where the engine climbs outward looking for a declaration — is called the scope chain.

Scope Chain in Action

var a = 'global';

function outer() {
  var a = 'outer';

  function inner() {
    // No `a` in inner's scope
    // → follows outerEnvironmentReference to outer's scope
    console.log('inner >>> ', a);
  }

  console.log('outer >>> ', a);
  inner();
}

outer();
console.log('global >>> ', a);
Enter fullscreen mode Exit fullscreen mode
outer >>>  outer
inner >>>  outer
global >>>  global
Enter fullscreen mode Exit fullscreen mode

Even though inner never declares a, it finds outer's a through the scope chain.

Notice the last line: global >>> global. The var a = 'outer' inside outer is a completely separate variable from the global a. outer declared its own a in its own scope, so the global a stays untouched.

Scope Is One-Way: Inside-Out Only

An important property of the scope chain is that it's unidirectional — inner scopes can see outer scopes, but not the other way around.

function outer() {
  var outerVar = 'outer';
}

console.log(outerVar); // ReferenceError: outerVar is not defined
Enter fullscreen mode Exit fullscreen mode

Trying to access outerVar from outside the function throws an error because the global scope has no visibility into outer's internals.


Summary

  • An execution context is an object holding the environmental info (VE, LE, ThisBinding) needed to run code.
  • A new execution context is created every time a function is called; it gets pushed onto the call stack and popped off when done.
  • The environmentRecord in LexicalEnvironment stores identifier info for the current scope.
  • The outerEnvironmentReference points to the outer environment where the function was declared, not where it was called.
  • Variable lookup starts in the current scope and walks outward through outerEnvironmentReference — this is the scope chain.

Closing Thoughts

Execution context is the foundation that ties together some of JavaScript's most important behaviors: hoisting, scope, and this binding all connect back to it.

In particular, the scope chain formed through outerEnvironmentReference is also the core mechanism behind closures — which I'll explore in a future post.

Thanks for reading!

👉 Read the full post on my blog

Read Full Tutorial open_in_new
arrow_back Back to Tutorials