completefrontend
Open lab My progress
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 which name is usedFollow how JavaScript looks for a name: first inside, then step by step outward.
  • 02
    Tell lexical from dynamic scopeExplain why calling a function doesn't give it the caller's variables.
  • 03
    Use the smallest scopeChoose global, module, function, or block scope on purpose for each variable.

Scope decides what names you can use

A name is the word you use for a variable, a constant, a parameter, or a function, like total. Scope is the part of a program where a name can be found.

Say you write console.log(total). Before JavaScript can print anything, it must answer one question. Which total can this line see? There might be several variables called total in your program, or none at all. Scope gives the answer.

This lesson covers:

  • Global scope: names at the top level of a script.
  • Function scope: names made inside a function.
  • Block scope: names made inside a pair of braces, { }.
  • The scope chain: how JavaScript searches outward, one scope at a time, to find a name.

You met some of this before. The Variables lesson showed that let stays inside its braces. The functions lesson showed a function reading a variable from outside it. Here you'll learn the full rule.

Real-life analogyScope is a set of rooms

You are in a bedroom. You can look through the door into the hall. From the hall, you can see the lobby. But a person in the lobby can't see into your bedroom.

Scope works one way, too. Code inside can use names from outside. Code outside can't use names from inside.

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 scope or the module scope
In real life: Things in each room
In JavaScript: The names each scope holds
The main idea

Lexical scope means this: the outer variables a function can use depend on where the function is written in your code. They don't depend on where it is called from.

Lexical scope: where code is written

Lexical means "about the written text". In JavaScript, the place where you write a function decides which outside variables it can use. If you write a function inside another function, the inner one can use the outer one's variables.

The Closures lesson builds on this. A function can only remember outer variables that it can see. So first you need to know which outer variables a function can see.

Scope only says where to look

Scope tells JavaScript where to search for a name. That's all. Other lessons cover other questions:

  • Can the value change? (Variables)
  • Can you read the name yet? (Hoisting & the temporal dead zone)
  • Does the value stay around after a function ends? (Closures)

Each function call has its own function scope. It holds the function's parameters and the variables made inside it.

Names made inside a function stay inside itPop out in the code editor (opens in a new tab)JavaScript
function greet(user) {
  const message = "Hi, " + user;
  return message;
}

console.log(greet("Asha"));

Line 1 gives greet a parameter called user. Line 2 makes a constant called message. Both exist only while greet runs. Line 6 gets back the finished string, Hi, Asha. It can't use user or message.

An ES module is a JavaScript file that uses import and export. Each module has its own module scope. Names at the top of a module belong to that file only. Other files can use them only if the module exports them.

Module scope keeps names in the fileJavaScript
// profile.mjs
const user = "Asha";
export function getUser() {
  return user;
}

// app.mjs
import { getUser } from "./profile.mjs";
console.log(getUser());

This example is two files, so the page can't run it in one editor. In profile.mjs, line 2 makes user, and line 3 exports getUser. In app.mjs, line 9 calls getUser(), which prints Asha. The app never sees user itself. It is not a global name for the whole page.

Global scope is the outermost scope. A name written at the top level of a normal script, outside every function and block, is global. Code anywhere in that script can use it.

A name at the top of a scriptPop out in the code editor (opens in a new tab)JavaScript
const siteName = "CompleteFrontend";

function showSite() {
  console.log(siteName);
}

showSite();

Line 1 makes siteName at the top level. Line 4 is inside showSite, and it can still read siteName. Line 7 calls the function, so it prints CompleteFrontend.

A global name can be read and changed by any code in the script. That makes mistakes easy, so keep globals few and make each one on purpose.

JavaScript also has a global object: one shared object that every script can reach. Its name is globalThis. It works in browsers and in Node.js.

Putting a value on globalThis on purposePop out in the code editor (opens in a new tab)JavaScript
const user = "Asha";
globalThis.cartCount = 3;

console.log(user);
console.log(globalThis.cartCount);

Line 1 makes a normal variable, user. Line 2 puts a property called cartCount on the global object. Line 4 prints Asha, and line 5 prints 3.

Sharing a value this way can help two scripts work together. But any other code can change it too, so a module that exports the value is usually safer.

A common mistake is giving a value to a name you never declared. To declare a name means to make it with let, const, or var. The line "use strict" turns on strict mode, a set of safer rules. In strict mode, giving a value to an undeclared name throws a ReferenceError.

Declare the variable inside the functionPop out in the code editor (opens in a new tab)JavaScript
function saveScore() {
  "use strict";
  const score = 10;
  console.log(score);
}

saveScore();

Line 2 turns on strict mode for saveScore. Line 3 declares score with const, so it belongs to saveScore only. Line 7 calls the function, and line 4 prints 10. Without the const, strict mode would throw a ReferenceError instead.

Lexical vs dynamic scope

INTERACTIVE

Calling a function doesn't give it your variables. In JavaScript, a function uses the variables from the place where it was written. Some other languages work differently. They use dynamic scope: the function uses the variables of whoever called it. JavaScript does not do that.

Call the function from another place
The function uses the label where it was writtenPop 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());
What prints

JavaScript prints: defined in makeReader

Dynamic scope would print: caller local

The caller has its own label, but readLabel was not written inside the caller, so it never looks there.

Try it yourself
JavaScript prints: defined in makeReader

readLabel is called from callFromAnotherPlace, which has label = "caller local". But readLabel uses the label from where it was written, inside makeReader. A language with dynamic scope would print caller local instead.

The output is real JavaScript. The dynamic scope line is only for comparison. JavaScript never prints it.

Line 13 makes a label inside the caller, callFromAnotherPlace. But readLabel was written inside makeReader, on lines 5 to 7. So it reads the label from line 4. The playground also shows what dynamic scope would print. That line is only for comparison. JavaScript never prints it.

The function readLabel at the top. An arrow goes to the left box called label, inside makeReader, which holds defined in makeReader. That is the value it reads. The right box, also called label, belongs to the caller and holds caller local. It is not used.readLabel()readslabelin makeReader"defined in makeReader"labelin the caller"caller local"
Two boxes are both called label. readLabel was written inside makeReader, so it reads the box in makeReader. The caller's box is never used, even though the caller is the one that runs readLabel().
What about this?

The word this follows a different rule. Its value usually depends on how a function is called, not where it is written. So this is not part of scope. The this keyword lesson covers it.

Block scope

PLAYGROUND

A block is code between braces, { }. It can be the body of an if, the body of a loop, or just a pair of braces on their own. Each block has its own block scope.

  • let and const belong to the nearest block around them.
  • The older var skips normal blocks. It belongs to the function around it, or to the whole script.
Which names last after the block?
let, const, and var inside an if blockPop 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
Where each name lives
Outside the block (the whole script)
functionVarvar ignores this block
Inside the if block
blockLetinside if
blockConstinside if

Line 7, console.log(functionVar), prints var ignores this block.

After the block, blockLet and blockConst are gone. Reading them throws a ReferenceError.

Try it yourself

var functionVar belongs to the whole script, not to the if block. So line 7 can still read it after the block.

The boxes are a drawing to help you think. The output comes from running the real JavaScript.
Left: inside the if block are blockLet and blockConst. They are gone after the closing brace. Right: outside the block is functionVar, because var ignores the block. Line 7 can read it.inside the if blockblockLetblockConstgone after }outside the blockfunctionVarline 7 reads it
blockLet and blockConst live only inside the if block. functionVar is written inside the block too, but var puts it outside, so line 7 can still read it.

There are four kinds of scope. Read each row as: "this kind of scope is made by this, and it is good for this."

The four kinds of scope in JavaScript
Kind of scopeWhat makes it?What is it good for?
GlobalThe top level of a script makes it.Use it only for names the whole script truly must share.
ModuleEach ES module file makes one.Use it for names that belong to one file and shouldn't become global.
FunctionEach function call makes a new one.Use it for the parameters and variables of that one call.
BlockBraces { } make one for the let and const inside them.Use it for short-lived values in an if, a for loop, or a plain block.

Give each variable the smallest scope it needs. If a value is only used inside an if or a for loop, declare it there. Don't declare it at the top of the whole function.

The scope chain

STEP THROUGH

When JavaScript reads a name, it first looks in the current scope. If the name isn't there, it looks in the scope just outside. It keeps stepping outward, one scope at a time, until it finds the name. This path from the inside out is called the scope chain.

If the search reaches the global scope and still finds nothing, reading the name throws a ReferenceError.

Three places in a row, joined by arrows. Line 7 asks for room inside inner. inner has no room, so JavaScript looks next in outer. outer has room, outer hall, so the search stops. The global room, global lobby, is never reached.return room;inner()no room herenextouter()"outer hall"found: stopnextglobal"global lobby"never reached
Line 7 asks for room. JavaScript looks in inner first and finds nothing. It steps out to outer, finds "outer hall", and stops. The global room is never reached.
Which room does line 7 return?
Step 0 of 7Ready
Your turn: follow the blue line

Guess which room line 7 will use. Then step through and watch the search go from inner outward.

Running in
  1. script
Next: line 1
 function outer() {  const room = "outer hall";  function inner() {    // no local room here    return room;  }  return inner();} console.log(outer());
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Line 6, inside 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.

Now pick the setting that adds const room = "inner desk" on line 6, and step through again. Only one line changed. But now inner has its own room, so the search stops right there and returns "inner desk". This is the main rule: the nearest name wins.

Shadowing

SORTER

Shadowing happens when an inner scope makes a name that an outer scope already has. Inside the inner scope, the inner name hides the outer one. This is allowed, and sometimes useful. But it can also hide the value you meant to read.

Real-life analogyThe nearest lamp with the same name

Your bedroom and the hallway both have a lamp called "light". When you say "turn on the light", you use the one beside you. The hallway lamp is still there. You just don't use it from your desk.

In real life: A lamp on your desk
In JavaScript: A name in the inner scope
In real life: A lamp in the hallway
In JavaScript: The same name in an outer scope
In real life: Using the closest lamp first
In JavaScript: The nearest name wins
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 piece of code, choose which x the line console.log(x) reads.

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

Some same-name code is not allowed at all. JavaScript stops it before running anything, with a SyntaxError. Here, var skips the block, so it tries to make x in the same scope where let already made x.

let and var with the same name: an errorPop out in the code editor (opens in a new tab)JavaScript
let x = "outer";
{
  var x = "same function scope";
}

Here is shadowing that works:

The inner color is gone after the closing bracePop out in the code editor (opens in a new tab)JavaScript
let color = "blue";
{
  let color = "green";
  console.log(color);
}
console.log(color);

Line 1 makes color with the value blue. Line 3 makes a second color inside the block, with the value green. Line 4 is inside the block, so it prints green. Line 6 is after the closing brace. The inner color is gone, so line 6 prints blue. Shadowing hides the outer name for a while. It never changes it.

Two boxes, both called color. Left: the box inside the block holds green, so line 4 prints green. Right: the box outside the block holds blue, so line 6 prints blue.inside the block"green"colorline 4 prints greenoutside the block"blue"colorline 6 prints blue
There are two boxes called color. Inside the block, the nearer box ("green") is used. After the closing brace, that box is gone, and line 6 uses the outer box ("blue").

Lexical environment records

The specification is the official rule book for JavaScript. It describes each scope as a lexical environment record. A record has two parts:

  • A list of the names in that scope, with their values.
  • A link to the scope just outside it. The spec calls this the outer reference.

Each function call gets a record. Each block gets a record for its let and const names. Each module gets a record. The scope chain is just these outer links, followed one after another.

A returned function still reads its outer variablesPop 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());

Line 8 calls makeGreeter("Ada"). That call gets a record holding name and punctuation. Line 3 returns the greet function, which keeps a link to that record. So when line 9 calls it, line 4 can still read both names, and it prints Hello, Ada!. The next lesson, Closures, explains this fully.

A way to think, not a memory map

Environment records describe how JavaScript behaves. Real engines, like the one in your browser, may store things in faster ways. They still give the same results.

Where you use this

Scope rules help you make everyday choices:

  • Declare a value that only a loop uses inside that loop. Then later code can't use it by mistake.
  • Keep helper functions in the same module as the data they read.
  • Declare every name with let, const, or var, so you never make a global by accident.
  • If an inner name hides an outer one and makes the code confusing, rename one of them.
A practical rule

Declare each variable in the smallest scope that holds every line that uses it. If you later need it somewhere else, move it outward on purpose.

Keep the cart shared, keep the count localPop out in the code editor (opens in a new tab)JavaScript
const cart = { items: [] };

function addToCart(item) {
  const itemCount = cart.items.push(item);
  return itemCount;
}

console.log(addToCart("tea"));
console.log(cart.items);

Line 1 makes cart outside the function, so every call to addToCart adds to the same list. Line 4 makes itemCount inside the function. The array method push adds an item and gives back the new length. Each call gets a fresh itemCount, so it never becomes shared data. Line 8 prints 1, and line 9 prints ["tea"].

Common misconceptions

“The caller decides which outer variables a function uses.”

No. A function uses the outer variables from where it is written. The caller can pass arguments, but it can't give the function its own variables.

“A block keeps var inside it.”

No. var skips normal blocks. Use let or const for a name that should stay inside a block.

“Shadowing changes the outer variable.”

No. Shadowing makes a second variable with the same name. The outer variable still exists, with its old value.

“Environment records show how memory really looks.”

No. They are the spec's way to describe behavior. Engines may store things differently and still give the same results.

“Using a name without declaring it is fine if it works.”

No. Outside strict mode, it quietly makes a global by accident. In strict mode, it throws a ReferenceError.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict three labels

There are three console.log lines. Predict what each one prints, in 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 a global made by accident

    This program prints 10, but only because line 2 makes a global score by accident. Fix it so score belongs to saveScore only.

    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 hidden name

      This greeting ignores the name you pass in, because it uses a nearer name instead. 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 into the loop

        Change the code 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 · ChallengeLook ahead to closures

          In one sentence, explain why an inner function can still read an outer variable after the outer function has finished. Then name the next lesson, which explains it in depth.

          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 inner 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 keywords make a variable that stays inside its block?

              Choose an answer to see the explanation.

            4. Question 4 of 7What does this code 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 7In the lesson's readLabel example, what would dynamic scope do?

              Choose an answer to see the explanation.

            7. Question 7 of 7What is a lexical environment record?

              Choose an answer to see the explanation.

            Key takeaways

            • Lexical scope means a function uses the outer variables from where it is written.
            • To find a name, JavaScript looks in the current scope first, then steps outward along the scope chain.
            • let and const stay inside their block. var skips normal blocks.
            • Shadowing means a nearer name hides an outer name with the same spelling. The outer one is unchanged.
            • The spec describes each scope as an environment record: a list of names plus a link to the outer scope.

            Final definition: Scope is the set of places JavaScript searches, from the inside out, to find a name.

            Up next: Closures, where a function keeps its outer variables after the outer function has finished.

            CompleteFrontend Clear concepts. Working examples.