PLAINFINCH learn by building

Scope

You will see where each variable can be used and keep a value alive between clicks with a closure.

What does scope actually decide?

Scope is the set of places in your code where a name can be used. When JavaScript meets a name such as price, it looks in the current scope first, then the scope around that, then the one around that, until it reaches the outermost level. If no scope declares the name, you get a ReferenceError: price is not defined.

The rule only works in one direction. Inner code can see outer variables, outer code cannot see inner ones. That lets a function keep its working variables private, and it means two functions can each have a variable called total without stepping on each other.

Scope is decided by where you write the declaration, not by where the code is called from. Look at the braces around a let or const and you know exactly who can see it.

const shop = "Spokes";

function greet() {
  const visitor = "Mo";
  console.log(`${visitor} visits ${shop}`);
}
greet();
console.log(typeof visitor); // "undefined": not visible out here
FigureThree nested boxes: global scope holds loaves and button, the function scope inside it holds price, and the block scope inside that holds note. An arrow from the block points outward: lookups go from inner to outer, never inwardThree nested boxes: global scope holds loaves and button, the function scope inside it holds price, and the block scope inside that holds note. An arrow from the block points outward: lookups go from inner to outer, never inward
Each pair of braces makes a scope inside the one around it. Code can read variables from its own scope and every scope outside it, but never from a scope nested inside.

How do braces make a block scope?

Any pair of curly braces that holds statements is a block: the body of an if, a for loop, a while loop, or even a bare { }. A variable declared with let or const inside a block exists from that line until the closing brace, and no further.

This catches people who declare a result inside an if and then try to use it after the closing brace. The Console reports message is not defined, which feels wrong because you can see the declaration. The fix is to declare the variable before the block, with a sensible default, and assign it inside.

Block scope is a good thing. It keeps short-lived values, like a loop counter or a temporary message, from leaking into the rest of the file where they could be reused by mistake.

const stock = 3;
let label = "In stock";
if (stock === 0) {
  label = "Sold out";
}
console.log(label);

What is function scope?

Every function call gets its own scope. Parameters and variables declared inside the body are created when the call starts and thrown away when it ends, unless something still refers to them (that is a closure, covered below). Call the function again and you get a fresh set.

That is exactly why the demo counter stuck at 1. The click function declared let loaves = 0 on every call, so each click started a brand new variable at zero. To keep a value between calls, the variable must live in a scope that outlasts the call: outside the function.

The old var keyword has function scope only. It ignores the braces of if and for, so a var declared inside a loop is visible in the whole function. That surprise is the main reason modern code uses let and const. You will still see var in older tutorials and in some AI answers; treat it as a sign the code needs a second look.

Why should you avoid lots of global variables?

A variable declared at the top level of script.js, outside every function and block, is global: every function in the file can read and change it. For a small page, a few globals such as the main elements and the app's state are fine.

Trouble starts when many functions write to the same globals. A bug in one function shows up somewhere else, and you have to read the whole file to learn who changed count. Globals also share a single space with every other script on the page, so two scripts that both declare const total at the top level collide with a SyntaxError.

Keep globals for the few things the whole page needs. Pass values into functions as arguments and get results back with return. When a value belongs to one feature, keep it inside a function or, later in the course, inside a module, which gives each file its own top-level scope.

What is shadowing, and when does it bite?

Shadowing happens when an inner scope declares a variable with the same name as an outer one. Inside the inner scope, the name means the inner variable, and the outer one is hidden. Both exist; you just cannot reach the outer one by that name from inside.

Shadowing is allowed and sometimes useful, but most of the time it is an accident. The classic case is the Break It exercise: a function writes let robins = 0; and counts into it, while the rest of the file reads the outer robins, which never changes. No error appears; the number is silently wrong.

When a value refuses to update, search the file for every declaration of that name. The Step lens shows each scope's variables separately, so two entries called robins with different values is a strong clue.

let total = 0;

function addCake(price) {
  let total = 0; // shadows the outer total
  total += price;
}
addCake(5);
console.log(total); // 0

What is a closure, in plain words?

A closure is a function that remembers the variables around it at the place it was written, and keeps using them even after the outer code has finished running. Every function in JavaScript does this; the word closure names the behaviour.

Click handlers rely on it constantly. The arrow function you pass to addEventListener runs long after the rest of the script has finished, yet it can still read output and loaves. It closed over them: it holds a live link to those variables, not a frozen copy of their values.

Live link is the important part. If the outer variable changes, the function sees the new value next time it runs. That is what makes the counter work, and it is also why a closure can surprise you if some other code changes the variable underneath it.

How do you build a counter with a closure?

A function that creates and returns another function gives each returned function its own private variable. Nothing outside can touch count directly; the only way to change it is to call the returned function. This keeps the count safe from accidental edits by other code.

Each call to makeCounter creates a fresh scope, so rye and spelt below count separately. Open the Step lens on this code and watch two separate count variables appear, one for each call.

You will meet the same pattern in real code under other names: a function that sets up some private state and hands back the functions allowed to use it.

function makeCounter() {
  let count = 0;
  return () => {
    count++;
    return count;
  };
}

const rye = makeCounter();
const spelt = makeCounter();
rye();
rye();
console.log(rye(), spelt()); // 3 1

Why does a loop with let give each pass its own variable?

A for loop declared with let creates a new copy of the loop variable for every pass. Any function created during that pass, such as a click handler or a timer callback, closes over that pass's copy and remembers its value.

With var there is only one variable for the whole loop. By the time the callbacks run, the loop has finished and the single variable holds its final value, so every callback reports the same number. This was a famous puzzle before let existed, and it still appears in older code.

Run the example below in the editor and look at the Console. With let you see 0, 1 and 2. Change let to var and you see 3 three times.

for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(`Oven tray ${i}`);
  }, 100);
}

What should you check in AI-written code about scope?

Look for var, which usually means the code was trained on older examples. Look for a variable declared inside an event handler that is supposed to remember something between events. Look for the same name declared twice at different levels, which is shadowing waiting to cause a silent bug.

Also check for variables that are used but never declared. In a normal script, assigning to an undeclared name creates a global without any warning, while in a module or strict mode it throws a ReferenceError. Either way, every variable should have a let or const somewhere you can point to.

Field notes

  • Scope decides where in your code a name can be used.
  • A let or const inside braces exists only inside that block.
  • Variables inside a function are made fresh on every call.
  • A closure is a function that keeps using the variables around where it was written.

Easy to get wrong

A const inside braces exists only inside those braces. Declare it before the block so the code after the block can read it.

A variable declared inside the handler is created again on every click, so it can never count past 1.

The inner let shadows the outer score, so the function changes its own copy and the outer value never moves.

Questions people ask

Is a closure something I have to create on purpose?

No. Every function remembers the variables in scope where it was written. You are using closures whenever a click handler reads a variable declared outside it. Writing a function that returns a function is one deliberate use of that behaviour.

Does a closure copy the variable's value?

It keeps a link to the variable itself. If the variable changes later, the closure sees the new value the next time it runs.

Can I see scopes in the browser?

Yes. In the editor, the Step lens lists the variables in each scope as you step. In browser DevTools, pause on a breakpoint and the Scope panel shows Local, Closure and Global sections.

Are top-level variables in script.js properties of window?

Top-level let and const are global but do not become properties of window. Old-style var and function declarations at the top level do, which is another reason to prefer let and const.

Keep going