Skip to main content

Lexical environment in JavaScript

Lexical environment is an internal JavaScript structure that links variable names to their values for a specific scope, and stores a pointer to the enclosing scope where the code was written.

Theory

TL;DR

  • Every function, block {}, and the global scope gets its own lexical environment when code runs
  • Each environment has two parts: Environment Record (the actual variables) and an outer reference (pointer to the parent scope)
  • Scope is determined by where you write the code, not where you call it
  • The chain of outer references is the scope chain
  • Closures work because functions keep a reference to the lexical environment where they were defined

Quick example

javascript
let city = "Kyiv"; function greet() { let name = "Anna"; console.log(name + " from " + city); // "Anna from Kyiv" } greet();

When greet runs, JavaScript creates a lexical environment for it. That environment holds name = "Anna" and has an outer reference to the global environment where city = "Kyiv" lives. greet finds city by walking up the chain.

Two parts of a lexical environment

The Environment Record stores the actual bindings: variables, function declarations, parameters. Think of it as a key-value map for that scope.

The outer reference points to the lexical environment of the code that surrounds the function in the source file. Not where the function is called. Where it is written.

javascript
function makeCounter() { let count = 0; // stored in makeCounter's Environment Record return function () { count++; // found via outer reference to makeCounter's environment return count; }; } const counter = makeCounter(); counter(); // 1 counter(); // 2

After makeCounter returns, its execution context is gone. But the inner function still holds the outer reference to makeCounter's environment. That environment survives because there is a live reference to it. That is a closure.

Lexical vs dynamic scoping

JavaScript uses lexical scoping. Variables are resolved based on where the code is written, not where it is called.

javascript
let x = 10; function log() { console.log(x); } function run() { let x = 20; log(); // prints 10, not 20 } run();

log was defined in the global scope. Its outer reference points to the global environment. The x = 20 inside run is invisible to log, because log does not look there.

Languages like Perl and Bash can use dynamic scoping, where variable lookup follows the call stack. JavaScript does not.

How the engine builds the chain

When a function is created (not called), the engine attaches the current lexical environment to it as [[Environment]]. When the function is called, a new lexical environment is created for that call, and its outer reference is set to whatever is stored in [[Environment]]. That is why scope always follows the definition site.

Common mistakes

Confusing lexical environment with execution context. Execution context is the runtime record of a function call. Lexical environment is one component of that context. Related, but not the same thing.

Assuming scope follows the call stack. Developers often expect log() called from inside run() to see run's variables. It does not. The chain is fixed at definition time.

Forgetting that var skips block environments. let and const create bindings in the block's environment record. var ignores block environments and binds to the nearest function environment.

javascript
{ let blockVar = "block-scoped"; var funcVar = "function-scoped"; } console.log(typeof blockVar); // "undefined" - not accessible outside the block console.log(funcVar); // "function-scoped"

Thinking the environment is destroyed when the function returns. The garbage collector removes it only when no references remain. A closure that is still alive keeps the environment alive.

Real-world usage

  • React hooks: useState, useCallback, and useEffect closures capture values from the lexical environment when they are created. The stale closure bug in useEffect - where the callback reads an outdated value - is probably the most common issue I see when teams move away from class components
  • Module pattern: IIFE-based modules keep private state in a closed-over lexical environment
  • Event handlers in loops: the classic var + setTimeout bug happens because all handlers share the same environment and read i after the loop finishes
  • Memoization: useMemo and manual memoize functions rely on closures over a cached result

Follow-up questions

Q: What is the difference between lexical environment and scope chain?
A: The scope chain is the chain of outer references between lexical environments. The lexical environment is the individual node. The chain is what JavaScript traverses when looking up a variable.

Q: When is a lexical environment created?
A: A new one is created every time a function is called or a block is entered. Recursive calls each get their own, so they do not overwrite each other.

Q: Can a lexical environment outlive the function call that created it?
A: Yes. If an inner function closes over it, the environment stays alive as long as that inner function is reachable. This is the mechanism behind closures.

Q: What happens to let variables before the line where they are declared?
A: They exist in the environment record but are uninitialized. Accessing them before the declaration throws a ReferenceError. This range is called the Temporal Dead Zone (TDZ).

Q: How does var behave differently from let regarding lexical environments?
A: var declarations are hoisted to the nearest function's environment record, not the block's. They are also initialized to undefined immediately, unlike let and const.

Examples

Closure keeping the environment alive

javascript
function makeAdder(x) { // x lives in makeAdder's Environment Record return function (y) { return x + y; // x is found via outer reference }; } const add5 = makeAdder(5); const add10 = makeAdder(10); console.log(add5(3)); // 8 console.log(add10(3)); // 13

makeAdder was called twice, so two separate environments exist, each with its own x. add5 and add10 are closures over different environments. That is why they produce different results from the same input 3.

The var-in-loop problem and the fix with let

javascript
// Bug: all callbacks share one environment, i = 3 by the time they fire for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // prints 3, 3, 3 } // Fix: let creates a fresh block environment each iteration for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // prints 0, 1, 2 }

With var, there is one binding in the enclosing function's environment. All three callbacks point to the same i. With let, the loop creates a fresh block environment each iteration with its own i, so each closure captures something different.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?