cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Scope & lexical environments

Understand global, function, and block scope, plus how JavaScript walks the scope chain to find each variable name.

By the end, you can
  • 01
    Predict name lookupTrace how JavaScript searches from inner code outward.
  • 02
    Separate lexical from dynamicExplain why callers do not change a function's outer variables.
  • 03
    Use the smallest scopeChoose global, module, function, or block scope intentionally.

Scope decides what names you can use

Scope is the part of a program where a name can be found. If you write console.log(total), JavaScript must answer a simple question before it can print anything: which binding named total is visible from this line?

The curriculum summary for this lesson is our map: global, function, and block scope, and how lookups climb the scope chain. You already saw block-scope basics in Variables and a first look at outer variables in Declaring & calling functions. Execution contexts gave names a home; Hoisting & the temporal dead zone explains why some names exist before a line can safely read them.

Real-life analogyScope is a set of rooms

From inside a bedroom, you can look through the doorway into the hall, then the lobby. People in the lobby cannot see into your bedroom. Scope has the same one-way feel: inner code can use outer names, but outer code cannot reach inner locals.

In real life: Bedroom
In JavaScript: An inner function or block
In real life: Hall outside the bedroom
In JavaScript: The outer function
In real life: Lobby
In JavaScript: The global or module scope
In real life: Name tags in each room
In JavaScript: Bindings in an environment record

Where the analogy stops: Rooms are physical places. JavaScript scopes are rules attached to code and function calls.

Working definition

Lexical scope means a function's available outer variables are decided by where that function is written in the source code.

Lexical scope: where code is written

Lexical comes from words and text. In JavaScript, ordinary variable lookup is tied to the written shape of your code. If a function is written inside another function, it can read the outer function's variables because that is where the function was built.

This is why the Closures lesson opens with nested functions. This lesson is the foundation for that one: before a function can remember outer variables, you need to know which outer variables it can see.

Lexical does not mean frozen

Scope decides where to search for a name. It does not say whether the value is mutable, whether the binding is in the temporal dead zone, or whether a closure will keep it alive. Those are related ideas, but not the definition of scope.

Lexical vs dynamic scope

INTERACTIVE

A caller does not get to lend its local variables to a function just by calling it. In JavaScript, the function reads the environment from where it was defined. Dynamic scope, used by some languages and tools, would instead ask who called the function.

Call from somewhere else
Lexical scope winsPop out in the code editor (opens in a new tab)JavaScript
const label = "global";

function makeReader() {
  const label = "defined in makeReader";
  return function readLabel() {
    return label;
  };
}

const readLabel = makeReader();

function callFromAnotherPlace() {
  const label = "caller local";
  return readLabel();
}

console.log(callFromAnotherPlace());
Lookup result

JavaScript prints: defined in makeReader

Hypothetical dynamic scope: caller local

The caller has a variable with the same name, but it is not on readLabel's lexical chain.

Try it yourself
Actual output: defined in makeReader

readLabel was called from a function with label = "caller local", but it reads the label from where it was defined. A dynamic-scope language would choose the caller local value instead.

This is real JavaScript output. The dynamic result is a labeled hypothetical contrast, not what JavaScript does.

The caller has its own label, but readLabel was created inside makeReader, so it reads that environment. The dynamic result in the playground is deliberately labeled hypothetical.

What about this?

JavaScript's this is the dynamic-ish part: it is usually decided by how a function is called. That is a different rule from lexical variable lookup and belongs in The this keyword lesson.

Block scope

PLAYGROUND

A block is code between braces: an if body, a loop body, or even a plain { ... } block. let and const live in the nearest block. The older var ignores ordinary blocks and belongs to the surrounding function or script.

Block scope visualizer
Block scope with let, const, and varPop out in the code editor (opens in a new tab)JavaScript
if (true) {
  let blockLet = "inside if";
  const blockConst = "inside if";
  var functionVar = "var ignores this block";
}

console.log(functionVar);
// console.log(blockLet); // ReferenceError outside the block
Environment boxes
Outer function/script record
functionVarvar ignores this block
if block record
blockLetinside if
blockConstinside if

console.log(functionVar) prints var ignores this block.

blockLet and blockConst are not available outside the block.

Try it yourself

var functionVar belongs to the surrounding function or script, so it is still readable after the if block.

The boxes are a conceptual model of environments; the output comes from real JavaScript.
Common scope kinds
Scope kindWhat creates itWhat it is good for
GlobalA script's top levelNames that truly must be shared by the whole script
ModuleAn ES module fileFile-level names that should not become globals
FunctionEvery function callParameters and variables used by one call
Block{ ... } around let and constTemporary values for an if, for, or plain block

Prefer the smallest useful scope. A temporary value used only inside an if or for block should usually be declared there, not at the top of a whole function.

The scope chain

STEP THROUGH

When JavaScript reads an identifier, it starts in the current environment. If the name is not there, it walks outward one environment at a time. That outward path is the scope chain.

Scope-chain lookup: which room is returned?
Step 0 of 7Ready
Your turn: follow the blue line

Predict which room line 7 will use. Then step through the lookup from inner to outer scopes.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
 function outer() {  const room = "outer hall";  function inner() {    // no local room here    return room;  }  return inner();} console.log(outer());
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Line 6 in inner
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Turn on the inner declaration and replay. One line changes, but the lookup result changes because the closest room now has its own name tag. That is the core rule: nearest binding wins.

Shadowing

SORTER

Shadowing happens when an inner scope declares a name already used by an outer scope. It is legal and often useful, but it can hide the value you meant to read.

Real-life analogyThe nearest lamp with the same name

If your bedroom and the hallway both have a lamp called light, you use the one beside you. The hallway lamp is still there; it is just hidden by the nearer name.

In real life: A lamp on your desk
In JavaScript: An inner binding
In real life: A lamp in the hallway
In JavaScript: An outer binding
In real life: Using the closest lamp first
In JavaScript: Nearest scope wins

Where the analogy stops: A real lamp does not prevent the hallway lamp from existing. Shadowing also hides only while you are inside the inner scope.

Which x is used?
  • let x = "global"; console.log(x);
  • let x = "global"; function run() { let x = "outer"; console.log(x); }
  • let x = "global"; { let x = "block"; console.log(x); }
  • let x = "global"; function run() { console.log(x); }
  • let x = "global"; { let x = "block"; } console.log(x);
  • function outer() { let x = "outer"; function inner() { console.log(x); } }
Try it yourself
0 of 6 correct

For each snippet, choose the binding that console.log(x) reads.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

JavaScript also has early errors for some confusing redeclarations. This one is a syntax error because var tries to create a function-scoped binding where let already declared the same name.

Illegal shadowing-like redeclarationPop out in the code editor (opens in a new tab)JavaScript
let x = "outer";
{
  var x = "same function scope";
}

Lexical environment records

The specification models scope with lexical environments. You can picture each environment as a record of bindings plus a reference to an outer environment. Function calls create function records; blocks create block records for let and const; modules create module records.

Environment records become important for closuresPop out in the code editor (opens in a new tab)JavaScript
function makeGreeter(name) {
  const punctuation = "!";
  return function greet() {
    return "Hello, " + name + punctuation;
  };
}

const greetAda = makeGreeter("Ada");
console.log(greetAda());

In the model, the returned greet function keeps a reference to the environment record that has name and punctuation. That is exactly the bridge to the next lesson, Closures.

Model, not microscope

Real engines optimize aggressively. Environment records are the language's precise model for behavior, not a promise that every engine stores boxes with these labels in memory.

Where you use this

Scope rules guide everyday decisions:

  • Put loop-only values inside the loop so later code cannot misuse them.
  • Keep helper functions near the data they read, especially in modules.
  • Avoid accidental globals by declaring every name with let, const, or var.
  • Rename shadowed values when the duplicate name makes code harder to read.
A practical rule

Declare a variable in the smallest scope that contains every line that needs it. If you later need it elsewhere, move it outward on purpose.

Common misconceptions

“The caller decides outer variables.”

No. Ordinary names are lexical. The call site can pass arguments, but it cannot rewrite the callee's scope chain.

“Blocks hide var.”

var ignores ordinary blocks. Use let or const for block-local names.

“Shadowing changes the outer variable.”

Shadowing creates another binding with the same name. The outer binding still exists.

“Environment records are engine screenshots.”

They are the spec's behavior model. Engines may represent optimized code differently.

“An undeclared assignment is fine if it works.”

In sloppy scripts it can create an accidental global. In strict mode it throws a ReferenceError.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict nested scope output

Read the three console.log calls and predict the output order.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const label = "global";
function outer() {
  const label = "outer";
  {
    const label = "block";
    console.log(label);
  }
  console.log(label);
}
outer();
console.log(label);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeFix an accidental global

    The program prints 10 in sloppy mode by creating an accidental global. Fix it so score is local.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function saveScore() {
      score = 10;
    }
    saveScore();
    console.log(score);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFind the shadowing bug

      This greeting ignores its argument because a nearer name gets used. Fix it.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const user = { name: "Ada" };
      function greet(userName) {
        const name = "friend";
        return "Hi, " + name;
      }
      console.log(greet(user.name));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeMove a variable to the smallest scope

        Refactor the starter so label is declared only where it is needed.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        function format(items) {
          let label;
          const prefix = "Item: ";
          for (const item of items) {
            label = prefix + item;
            console.log(label);
          }
        }
          Exercise 5 · ChallengeExplain the closure teaser

          In one sentence, explain why an inner function can read an outer variable after the outer function has returned. Which next lesson goes deeper?

          Answer, then press Check. Spacing and letter case don’t matter.

            Quiz

            7 QUESTIONS
            Scope quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What does lexical scope mean?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does the nested function print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const x = "global";
              function outer() {
                const x = "outer";
                function inner() {
                  console.log(x);
                }
                inner();
              }
              outer();

              Choose an answer to see the explanation.

            3. Question 3 of 7Which declaration is block-scoped?

              Choose an answer to see the explanation.

            4. Question 4 of 7What does the block-scope snippet print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              let topic = "global";
              {
                let topic = "block";
              }
              console.log(topic);

              Choose an answer to see the explanation.

            5. Question 5 of 7What is shadowing?

              Choose an answer to see the explanation.

            6. Question 6 of 7What would dynamic scope do in the lesson's contrast?

              Choose an answer to see the explanation.

            7. Question 7 of 7What is a lexical environment record in this lesson's model?

              Choose an answer to see the explanation.

            Key takeaways

            • Lexical scope is decided by where code is written.
            • Lookup starts locally and walks outward along the scope chain.
            • let and const are block-scoped; var is not.
            • Shadowing means a nearer binding hides an outer binding with the same name.
            • Environment records are the spec model: bindings plus an outer reference.

            Final definition: Scope is the set of lexical environments JavaScript searches to resolve a name.

            Up next: Closures.

            CompleteFrontend Clear concepts. Working examples.