Scope
The rules that decide where a variable can be used in your code.
What is it?
A variable can't always be used everywhere in your program — there are places where a given variable simply doesn't exist as far as the code is concerned. This area where a variable is usable is called its scope.
JavaScript decides scope based on where in the code a variable was declared — this is called lexical scope. A variable declared inside a function is only usable inside that function (and anything nested within it); it disappears once the function finishes.
Explain like I'm 10
Think of scope like rooms in a house. Something you leave in the kitchen is only reachable while you're in the kitchen or in rooms that connect to it — it's not automatically available in every room of the house.
Examples
Function scope
function greet() {
const message = "Hello!";
console.log(message); // works fine, "message" is in scope here
}
greet();
console.log(message); // ❌ Error: message is not defined out hereBlock scope with let/const
if (true) {
const secret = "hidden";
console.log(secret); // works
}
console.log(secret); // ❌ Error: secret is not definedHow it works
When JavaScript looks up a variable, it checks the current block first, then the block that contains it, and so on outward — this chain is called the scope chain. If it reaches the outermost level without finding the variable, it throws an error. This lookup is based entirely on where the code was written, not on the order things happen to run in.
Global scope
└── Function scope
└── Block scope (if/for/while)
Lookup direction: inner → outer, never outer → innerWhy does it exist?
Without scope, every variable in a program would be visible everywhere, which would make large programs a mess — names would collide constantly, and it would be impossible to tell what any given piece of code depends on. Scope keeps variables contained to where they're actually relevant.
When to use it
You're actively reasoning about scope any time you're deciding where to declare a variable, debugging a "not defined" error, or trying to understand why a variable inside a function isn't visible outside it.
When not to use it
You don't need to think hard about scope for a variable used in one small, self-contained block — it only becomes a source of confusion (and bugs) once a program grows large enough that the same name gets reused in multiple places.
Common mistakes
Assuming a variable declared inside an
ifblock is available outside of it.Accidentally creating a global variable by forgetting
let/const/var.Being surprised that two functions can each have their own separate variable with the same name, with no conflict.
Practice exercises
- Easy:
Declare a variable inside a function and try (and fail) to log it outside the function. Observe the error.
- Medium:
Write two functions that each declare a local variable with the same name, and show they don't interfere with each other.
- Hard:
Explain, in writing, why a variable declared with
letinside aforloop is not accessible after the loop ends.
Interview questions
What is scope in JavaScript?
The set of rules that determines where in your code a given variable is accessible — declared inside a function, it's usable there and in anything nested within it, but not outside.
What does it mean that JavaScript uses lexical scope?
A variable's accessibility is determined by where it's physically written in the source code, at the time it's defined — not by which function happens to call which, or the order code runs in at runtime.
What's the difference between global scope, function scope, and block scope?
Global scope is visible everywhere; function scope is limited to inside a function (and anything nested in it); block scope, introduced by let/const, is limited to the nearest enclosing {} — an if, for, or bare block.
What is the 'scope chain', and how does variable lookup use it?
When code references a variable, the engine checks the current scope first; if it's not found there, it checks the scope that contains it, and so on outward until it either finds the variable or reaches the global scope and throws a ReferenceError. It only ever searches outward, never inward.
What does this log? `const a = 1; function outer() { const a = 2; function inner() { console.log(a); } inner(); } outer();`
2. inner has no a of its own, so the lookup walks outward along the scope chain to outer's scope, finds a = 2 there, and stops — it never continues out to the global a.
What does this log? `console.log(x); var x = 5;`
undefined, not a ReferenceError. var declarations are hoisted to the top of their enclosing function (or global) scope and initialized to undefined immediately; only the assignment (x = 5) stays in place, so reading x before that line sees the hoisted-but-unassigned value.
What does this log? `console.log(y); let y = 5;`
It throws ReferenceError: Cannot access 'y' before initialization. let/const are hoisted too, but unlike var they aren't initialized to undefined — they sit in a 'temporal dead zone' from the top of the block until their declaration actually runs, and touching them there is an error.
What's the practical difference between `var` being function-scoped and `let`/`const` being block-scoped?
A var declared inside an if or for block leaks out and is accessible anywhere in the enclosing function; a let/const declared the same way only exists inside that specific {} block and disappears once it ends.
What does this log? `for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); }`
3, 3, 3. var is function/global scoped, so there's only ever one i shared by every iteration; by the time the deferred callbacks actually run, the loop has already finished and i is 3.
What does this log, and why does it differ from the `var` version? `for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); }`
0, 1, 2. let creates a fresh binding of i scoped to each loop iteration, so each closure captures a different variable holding a different value, instead of all of them sharing one.
What happens if you forget `let`/`const`/`var` when assigning to a new variable name inside a function?
In non-strict mode, JavaScript silently creates an accidental global variable rather than erroring — the assignment succeeds, but the variable is now visible everywhere, not just inside that function.
What does this log? `function foo() { bar = 5; } foo(); console.log(bar);`
5 in non-strict, non-module code — bar was never declared, so the assignment creates it as a global. In strict mode or an ES module, this instead throws a ReferenceError, which is one reason strict mode/modules are considered a safer default.
What does this log? `if (true) { const secret = "hidden"; } console.log(secret);`
ReferenceError: secret is not defined — const (and let) are block-scoped, so secret only exists inside the if block's {} and is gone the moment execution leaves it.
What is variable shadowing?
Declaring a variable with the same name as one in an outer scope. Inside the inner scope, the new declaration takes over and hides (shadows) the outer one entirely, without modifying or being affected by it.
What does this log? `let x = "outer"; function show() { let x = "inner"; console.log(x); } show(); console.log(x);`
"inner" then "outer" — the x inside show shadows the outer x for the duration of the function, but the outer x is completely untouched once show returns.
Does an ES module's top-level scope behave like the classic global scope from a `<script>` tag?
No — each module has its own private, file-level scope. A top-level const/let/function in a module isn't visible in other files unless explicitly exported, unlike a classic script where top-level var/function declarations become properties of the shared global object.
What was an IIFE used for before `let`/`const` existed, and why is it less necessary now?
An Immediately Invoked Function Expression created a private function scope on demand, faking the block-scoping that var couldn't provide — used to keep helper variables from leaking into the global scope. let/const now give you real block scope directly, without wrapping code in a function.
What does this log? `(function () { var secret = "hidden"; })(); console.log(typeof secret);`
"undefined" — secret is scoped entirely to the IIFE's own function scope and ceases to exist once that function finishes running; it was never visible outside.
How is 'scope' different from 'execution context'?
Scope is a static property of the code — which variables a piece of code can see, fixed by where it's written. Execution context is the runtime state of a specific function call — its this binding, its arguments, and the variable environment created fresh for that call.
Does JavaScript use lexical scoping or dynamic scoping?
Lexical scoping — a function's variable scope is fixed by where the function is defined in the source code, and never changes based on where or how it's later called.
What does this log? `let value = "global"; function a() { console.log(value); } function b() { let value = "local"; a(); } b();`
"global". Even though a() is called from inside b, where a local value exists, a's scope was fixed when it was defined — at the top level, where it can only see the global value. If JavaScript used dynamic scoping, it would print "local" instead.
Is a caught error in a `catch` block scoped like a regular `let` declaration?
Yes — the parameter in catch (err) { ... } is block-scoped to that catch block alone, so it doesn't exist before the catch or after it, and doesn't collide with an outer variable of the same name.
What does this log? `function greet(name, greeting = "Hello, " + name) { console.log(greeting); } greet("Amara");`
"Hello, Amara" — default parameter expressions are evaluated in their own scope that can see parameters declared before them, so greeting's default can reference name.
Why does scope matter more as a codebase grows, even though a single small function rarely needs to think about it?
Without scoping, every variable name would have to be unique across the entire program to avoid collisions; scope lets the same short, obvious name (i, result, data) be reused safely in many unrelated functions, because each one only sees its own copy.
Why is it usually a bad sign to see a variable declared far from where it's used, at a broader scope than necessary?
The wider its scope, the more code can accidentally read or overwrite it, and the harder it becomes to reason about what value it holds at any given point — keeping declarations as narrowly scoped as possible limits how much code you need to check when debugging it.
What does this log? `console.log(typeof undeclaredVar); console.log(undeclaredVar);`
"undefined", then a ReferenceError. typeof is specially safe on an undeclared identifier and just reports "undefined"; actually evaluating the identifier by referencing it directly throws, because it was never declared in any accessible scope.
What is the relationship between scope and closures?
A closure is what you get when a function is defined inside another and keeps access to that outer function's scope even after the outer function has returned — it's the scope chain persisting longer than the call that created it, rather than a separate mechanism.
How would you debug a `ReferenceError: x is not defined`?
Check that x is actually declared somewhere in a scope that contains the line where it's used — a typo in the name, a let/const declared inside a block or later in the same block (temporal dead zone), or a variable meant to be a parameter or import that was never wired up, are the usual causes.
Can two completely separate functions each declare a local variable with the same name without conflict?
Yes — each function call gets its own fresh scope, so a let total inside one function and a let total inside a totally unrelated function are different bindings that never interact, even though they share a name.
Why can't you use `const` for a traditional counting `for` loop like `for (const i = 0; i < 10; i++)`?
The loop's increment step (i++) reassigns i on every pass, and const forbids reassigning the variable it's bound to — so this throws a TypeError immediately on the first increment. for...of loops can use const because a new binding is created for each iteration rather than reassigning one.
Are function declarations hoisted the same way `var` is?
Further than var — a function declaration is hoisted with its entire definition already attached, so it can be called before the line it's written on. var is hoisted but only initialized to undefined, so calling it early would fail.
Does declaring the same `var` name twice in the same scope cause an error?
No — redeclaring a var in the same scope is simply ignored (it's treated as the same variable, keeping its current value); doing the same with let/const throws a SyntaxError for an illegal redeclaration.
What's the scope of a function's parameters relative to its body?
Parameters live in their own scope level that wraps the function body — the body can read and reassign them freely, and a let/const/var inside the body with the same name as a parameter shadows it within that inner scope.