Functions: naming and reusing behaviour · Lesson 19 of 77 · concept
Scope and shadowing
About 55 minutes.
By the end of this lesson you can
- Say which parts of a program can see a given name, and which cannot.
- Explain what a block is and what block-scoped means for let and const.
- Predict what an inner declaration does to an outer one with the same name.
- Keep a function's working values local instead of reaching out and changing the program around it.
- Recognise the two symptoms of a name that is out of scope.
Scope answers one question: who can see this name?
Explanation
Every name you declare is visible to some parts of your program and invisible to the rest. Scope is the word for the region where a name works. It is not an advanced topic — you have been relying on it since your first function — but it is the topic that decides whether a program stays understandable as it grows.
The reason to care is not tidiness. If every name were visible everywhere, then every function could quietly interfere with every other, and understanding one line would mean reading the whole program. Scope is what lets you open one function and reason about it on its own.
Blocks, and what block-scoped means
Explanation
A block is a pair of curly braces and everything between them. A function body is a block. The braces after an if are a block. So are the braces after an else, and a bare pair of braces on their own.
Names declared with let or const belong to the block they were declared in. When execution leaves that block, the name is gone — not hidden, gone. This is what block-scoped means, and it is why two different if branches can each declare a total without arguing.
A function's parameters follow the same rule: they belong to the function body and exist nowhere else. That is why the caller's variable and the parameter can share a name, or not share one, without anything being affected either way.
You can look outwards, never inwards
Explanation
Code inside a block can read names declared outside it. A function can use a const declared above it at the top level of the file, without that value being passed in. This is why the worked examples in earlier lessons could mention things declared before them.
The reverse is not true. Nothing outside a block can see a name declared inside it. A function's local values are private to it, and that is the property the whole idea rests on: you can rename a local variable in one function without checking a single other line of the program.
Which outer names a piece of code can see is decided by where it is written in the source, not by who calls it. Two functions declared at the top level of a file can both see the file's top-level names, no matter which of them calls the other.
Rooms and windows
An analogy
Picture each block as a room built inside a larger room. Every room has a window facing outwards, so from inside you can see everything in the room around you, and everything in the room around that.
The windows are one-way. Standing in the outer room, you cannot see into the rooms built inside it. And when a small room is demolished — when execution leaves the block — everything that was only in that room goes with it.
Shadowing: the same name, deeper in
Explanation
If you declare a name inside a block and that name already exists outside, the inner declaration wins inside that block. Every mention of the name in there refers to the inner one. This is called shadowing: the inner name casts a shadow over the outer one.
The crucial part is that shadowing does not modify the outer name. It hides it, temporarily and locally. When the block ends, the outer name is exactly as it was — same value, untouched. The second worked example below shows three values living under one name at three depths, each unaffected by the others.
Shadowing is not automatically a mistake. A short helper that uses total for its own running total is fine even if the file has a total elsewhere. It becomes a mistake when you shadow by accident and then wonder why the outer value never changed — which it never did, because you were writing to a different variable the whole time.
Reaching out and changing things
A common mistake
The opposite error is worse. Instead of declaring your own local name, you assign to one that lives outside the function. It works the first time, so it feels correct.
The trouble arrives on the second call. The function now starts from whatever the last call left behind, so the same arguments produce a different answer — and every other part of the program that reads that variable has silently had the rug pulled out from under it. Bugs of this kind are hard to find precisely because the broken function looks fine in isolation; the damage shows up somewhere else entirely.
The rule that avoids nearly all of this: if a function needs a value, take it as a parameter. If a function produces a value, return it. Reaching out to grab or change something is the exception and needs a reason. Lesson seven turns this into a proper principle.
The two symptoms of a name that is out of scope
A mental model
The loud symptom is a ReferenceError saying the name is not defined. Something used a name that does not exist where it stands. Ask where the name was declared, then find the closing brace between there and here.
The quiet symptom is typeof reporting "undefined" for a name you are sure you declared. typeof is deliberately forgiving: it answers "undefined" for a name that was never declared at all instead of throwing. The worked examples use this to prove that locals really have vanished from the outside — a name being invisible and a variable holding undefined look identical through typeof, which is worth remembering when you use it to debug.
Recap
Recap
A block is a pair of braces, and let and const belong to the block they were declared in. Inner code can read outer names; outer code can never see inner ones. Redeclaring a name inside a block shadows the outer one for that block only and leaves it unchanged. Assigning to an outer variable from inside a function is a different thing entirely, and is the source of bugs that appear far from their cause.
Worked examples
Every output below was produced by actually running the code in the same sandbox your exercises run in — it is not a prediction.
A function reading a name from outside itself
Two directions in one program. The function can see the file-level const above it; the file cannot see the const inside the function.
1const shopName = "Novus Coffee";2 3function printSign() {4 const suffix = " — open today";5 console.log(shopName + suffix);6}7 8printSign();9console.log(typeof suffix);- line 4
suffix is declared inside the function body, so it belongs to that block and disappears when the call finishes.
- line 5
shopName was never passed in. The function can see it because the function is written inside the region where shopName lives.
- line 9
Outside, suffix does not exist, so typeof answers "undefined" rather than throwing. This is the quiet symptom.
What it prints
Novus Coffee — open todayundefined
One name, three values, no interference
Shadowing at three depths. Read the output from the inside out and notice that the outer values are completely unharmed by the inner declarations.
1const label = "outside";2 3function report() {4 const label = "inside the function";5 if (true) {6 const label = "inside the if block";7 console.log(label);8 }9 console.log(label);10}11 12report();13console.log(label);- line 4
This shadows the file-level label for the whole function body. The file-level one is hidden here, not changed.
- line 6
A third declaration, in the if block. From line 6 to line 8, label means this one.
- line 9
Outside the if block again, so the if block's label is gone and this prints the function's own.
- line 13
Back at the top level. label is still "outside": nothing that happened inside the function touched it.
What it prints
inside the if blockinside the functionoutside
Parameters and locals do not survive the call
Where the working values of a function go when it finishes: nowhere. Both the parameter and the local const are invisible from outside, which is what makes a function safe to reason about on its own.
1function tally(points) {2 const bonus = 5;3 return points + bonus;4}5 6console.log(tally(10));7console.log(typeof points);8console.log(typeof bonus);- line 1
points is a parameter, which is a local name of this function body — nothing outside can reach it.
- line 6
The call runs, produces 15, and its local names are discarded.
- line 7
Both typeof lines answer "undefined" because neither name exists out here. Every call gets a fresh, private set of them.
What it prints
15undefinedundefined
Check yourself
Not scored, not stored. Getting one wrong is the useful part.
There are two variables called total here. What does the program print?
let total = 0;function addTwice(n) { let total = 0; total = total + n; total = total + n; return total;}console.log(addTwice(3));console.log(total);
A file declares const mode at the top. A function declares const mode inside its body and assigns it a different value. What is the file-level mode after the function has run?
A function declares const step inside its body. Which code can use step?
Write it yourself
Exercise · core · 6 tests
Stop the function changing the world around it
The starter looks right and passes its first test, then falls apart. scoreAfterPenalty is supposed to answer a question — what would the score be if this penalty were applied — but instead it applies the penalty for real by assigning to currentScore, so every call starts from wherever the last one stopped. Rewrite the function so that it reads currentScore, works out the answer in a name of its own, and returns it. Leave the declaration of currentScore exactly as it is: the tests check that it is still 100 at the end.
scoreAfterPenalty(penalty) returns currentScore minus penalty as a number, and gives the same answer every time it is called with the same penalty, in any order. It may read currentScore but must never assign to it, so currentScore is still 100 after any number of calls. The function returns; it does not print.
Define currentScore and scoreAfterPenalty at the top level of your code — the tests call them by name.
Worth remembering
- Which way does visibility flow between blocks?
Outwards only. Code inside a block can read names declared outside it; nothing outside can see names declared inside. let and const belong to the block they were declared in.
- What does declaring an existing name inside a block do to the outer variable?
Nothing. It creates a separate variable that hides the outer one for that block only. When the block ends, the outer value is exactly as it was.
Check this lesson against the source
We wrote the explanation; we did not invent the facts. This is the page that backs them.
MDN — let (JavaScript reference)
Check there: The page states that let declarations are scoped to the block they appear in, that a let in an inner block shadows an outer variable of the same name, and that accessing the name before its declaration throws a ReferenceError.
Check there: That const is block-scoped in the same way as let.
Check there: That var is function-scoped rather than block-scoped — the older behaviour you will meet in existing code, and the reason let and const exist.
Prefer to read the source in a structured breakdown? Turn this doc into a breakdown