Dataslope logoDataslope

Scope and the Scope Chain

The rules that decide *which variable a name refers to* — and the mental model that makes them obvious.

When your program writes the name x, the runtime has to figure out which x you mean. There may be many. The rules for that lookup are called scope, and they are simple once you have the right picture.

What scope is

A scope is a region of the program where a particular set of names is "in effect". When the runtime sees a name, it asks: is that name defined in the current scope? If not, what about the scope around it? And the one around that?

JavaScript has three kinds of scope:

  • Global scope — the outermost level. Variables declared here are visible everywhere.
  • Function scope — created when a function runs. Each call to a function gets its own.
  • Block scope — created by any { ... } block. Each if, for, or stand-alone block has its own.

Variables declared with let and const respect block scope. Variables declared with the old var only respect function scope, which is one of the reasons we avoid var.

A first example

Code Block
JavaScript ES2023+

The function's scope can "see out" to the global scope, but the global scope cannot see "in" to the function. Scopes are like one-way mirrors.

Block scope in action

Code Block
JavaScript ES2023+

The inner const x shadows the outer one. The two are separate variables that happen to share a name. The moment the if block ends, the inner x vanishes and the outer x is again the only one in scope.

The scope chain

When a name is referenced and the runtime cannot find it in the current scope, it looks in the enclosing scope, then the scope enclosing that, and so on out to the global scope. That sequence of nested scopes is called the scope chain.

To resolve a name, the runtime walks up this chain, taking the first match it finds.

Code Block
JavaScript ES2023+

stamp is found in the block. greeting is not in the block, not in the function — found in the global. name is not in the block, found in the function. The runtime walks outward until it succeeds.

Each function call gets its own scope

This is critical. Every time you call a function, a fresh scope is created for that call. Local variables from one call do not leak into another.

Code Block
JavaScript ES2023+

Each call starts with n = 0, increments it to 1, returns, and the local n is then discarded. We will see in the closures chapter how to make n persist across calls — that's a more advanced trick.

Shadowing: same name, different scopes

When a name is declared in an inner scope that already exists in an outer scope, the inner one shadows the outer one — the outer is still there, just hidden inside the inner scope.

Code Block
JavaScript ES2023+

This is sometimes intentional (a local variable that's conceptually independent of the outer one), but more often it's a source of subtle bugs. Avoid shadowing unless you have a good reason.

Lexical scope

A crucial property of JavaScript: scope is lexical, meaning it is determined by where the code is written, not by who calls whom. The scope chain of a function is fixed when the function is defined.

Code Block
JavaScript ES2023+

The output is "country is Wonderland". Even though describe() is called from caller(), the country it sees is the one visible from where describe was written — the global scope.

This is lexical scoping, and it is the foundation of closures (coming next). If JavaScript used dynamic scope instead, describe() would have looked up the caller's country and printed "Looking-Glass-Land". Lexical scope is far easier to reason about — and it is what JavaScript (and almost every other modern language) uses.

Why globals are dangerous

A variable declared at the top level is visible everywhere. That sounds convenient, but it means any part of the program can read or modify it, and that any function might silently depend on it. Big programs with many globals become impossible to reason about.

The general rule: declare variables in the narrowest scope you can. Inside a function. Inside a block. As close to where they are used as possible.

Code Block
JavaScript ES2023+

The "better style" example is more verbose, but every change to c is right there in the visible call sequence. Nothing can sneak in.

Hoisting (a short note)

You will sometimes hear that JavaScript "hoists" declarations. For modern code (using let, const, and functions declared with function), the important things to remember are:

  • A function NAME() { ... } declaration is fully usable anywhere in its enclosing scope, including before its declaration.
  • A let or const is only usable after the line on which it is declared. Using one earlier produces a "temporal dead zone" error.
Code Block
JavaScript ES2023+

Function hoisting is convenient. The let/const rule prevents a class of bugs where you accidentally use a variable before you've given it a value.

Putting it together: visualising the chain

Below is a slightly bigger example. Before you press Run, predict what each console.log will print, given the scope rules above.

Code Block
JavaScript ES2023+

Two things to notice:

  1. f remembers the variables from outer even though outer has long since finished. That memory is called a closure, the topic of the next page.
  2. The lookups for name, greeting, and planet are happening through the scope chain that was set up when inner was defined — not when inner is called.

Challenge

Challenge
JavaScript ES2023+
Spot the shadow

The function compute below has a subtle scope problem: a local variable accidentally shadows another. Without changing the test, modify the function so that the outer total (declared at the top of the function) is the one updated and returned.

The function should return the sum of 1 + 2 + 3 which is 6. As-is, it returns 0.


QuestionSelect one

In JavaScript, when the runtime needs to resolve a variable name, where does it look?

Only in the current function's scope

Only in the global scope

First in the current scope, then in the enclosing scope, and so on outward through the scope chain until either it finds the name or reaches the global scope

Wherever the function was called from (dynamic scoping)

On this page