JavaScript Scopes
Every time you've seen a 'variable is not defined' error or a value that mysteriously changed, it was a scope issue. Understanding scope means you stop guessing why your code breaks and start writing code that works the first time.
How JavaScript reasons about a variable
- Look nearby: search the current block or function for the declaration.
- Move outward: if it is not there, search the enclosing function, then global/module scope.
- Stop at TDZ: a
letorconstbinding exists from the start of its block but cannot be read before its declaration line. - Keep a closure alive: if an inner function is returned, it retains access to the outer variables it uses even after the outer call finishes.
let language = "JavaScript";
function makeCounter() {
let count = 0; // outer function scope
return function next() { // closure keeps count reachable
return ++count;
};
}
const nextCount = makeCounter();
console.log(nextCount()); // 1
console.log(nextCount()); // 2
{
// console.log(value); // ReferenceError: value is in the TDZ
let value = "block value";
console.log(value); // works after declaration
}
The temporal dead zone is about when a binding may be accessed. A closure is about retaining access to an outer lexical environment, not copying its values into the inner function.
When debugging: Where was the function written? Which scope declares the name? Has thelet or const declaration run? Is a returned callback keeping outer data reachable?
Scope determines where a name can be accessed in JavaScript.
Code can use bindings from its current scope and, when one is not found,
from enclosing scopes. It cannot reach a binding that belongs only to a
nested scope. The exact rules also depend on how the binding was declared,
because var, let, and const do not have
identical scope and initialization behavior.
Scope is what keeps a variable local to the code that needs it. It reduces naming conflicts, limits accidental changes, and makes it easier to reason about which value a name refers to.
The main scopes you will work with are global scope, function scope, block scope, and, in modules, module scope. The rest of this tutorial shows how JavaScript finds a name and how each scope behaves.
// 'name' is accessible here because it is in the global scope
let name = "JavaScript";
function greet() {
// 'name' is accessible inside this function too
console.log("Hello, " + name);
}
greet(); // Output: Hello, JavaScript
Scoping is the process JavaScript uses to resolve a name to a binding. When code runs, the engine searches nested lexical environments using consistent rules.
JavaScript checks the current scope first, then moves outward one level at a time. If it cannot find the name, it throws a ReferenceError. If an inner scope declares the same name, that binding shadows the outer one while the inner scope is active.
let language = "JavaScript";
function outer() {
let framework = "React";
function inner() {
// 'inner' can access 'framework' from outer() and 'language' from global scope
console.log(language); // Output: JavaScript
console.log(framework); // Output: React
}
inner();
}
outer();
JavaScript uses lexical scoping, also called static scoping. The place where you write a function determines which outer bindings it can access, not the place where you call it.
A function can reach bindings in its own scope and in the parent scopes that surround its definition. This scope chain is the foundation of closures.
function outerFunction() {
let outerVar = "I am from outer function";
function innerFunction() {
// innerFunction can access outerVar because of lexical scoping
console.log(outerVar);
}
return innerFunction;
}
const myFunc = outerFunction();
myFunc(); // Output: I am from outer function
Notice that innerFunction can still
reach outerVar even after
outerFunction has finished running.
Lexical scoping remembers the scope where the
function was originally written, not where it is
called.
JavaScript has three common types of scope, plus
module scope when you use import and
export:
- Global Scope: Variables declared outside any function or block.
-
Function Scope: The scope created
by a function. Variables declared with
varbelong to the nearest function;letandconstalso stay inside the function unless a smaller block limits them further. -
Block Scope: Variables declared
inside a block (
{}) usingletorconst.

JavaScript modules also have their own module scope. Top-level bindings in one module do not automatically become global or appear in another module.
The next sections look at each scope in detail.
A binding has global scope when it is available throughout the relevant script or application environment. In a classic browser script, a top-level declaration can be global. In an ES module, top-level declarations belong to the module scope instead and are not automatically global. Code in nested functions and blocks can read a global binding unless a nearer binding with the same name shadows it.
A small number of deliberate globals can be useful for shared configuration or browser APIs. Too many globals let unrelated code change the same state, which creates naming conflicts and makes a growing program harder to debug.
Keep application globals to a minimum. Prefer a module, function, or block binding where possible so the value's ownership and lifetime are clear.
// Global variable
let color = "blue";
function printColor() {
console.log(color); // Output: blue
}
printColor();
console.log(color); // Output: blue
Global variables are handy for sharing data, but too many can cause naming conflicts and unexpected changes. Keep shared values deliberate and localize everything else.
A function creates its own function scope. Code inside the function can use its local bindings, but code outside cannot access them directly. A nested function can read bindings from its containing function through the scope chain.
Values created inside a function stay private unless the function returns a value or exposes a function that uses them. Returning a value sends a result out, while returning an inner function can preserve access through a closure.
Function scope limits where a binding can be changed and makes its purpose easier to understand. If a value is needed only inside one function, declare it there.
function showMessage() {
let message = "Hello from inside the function!";
console.log(message); // Output: Hello from inside the function!
}
showMessage();
console.log(message); // ReferenceError: message is not defined
Inside a function, var is
function-scoped, while let and
const are block-scoped. In a simple
function body without inner blocks, all three are
available throughout that function.
Nested functions can access variables from their parent function. When an inner function keeps using those variables after the outer function has finished, this relationship is called a closure:
function outer() {
let x = 10;
function inner() {
let y = 5;
console.log(x + y); // Output: 15
}
inner();
console.log(y); // ReferenceError: y is not defined
}
outer();
Block scope applies to bindings
declared with let and const.
A block is code wrapped in curly braces
{}, such as an if statement,
a for loop, or a while loop.
Variables declared with let or
const inside a block stay inside that
block. var ignores block boundaries and
is function-scoped instead. A let or
const binding is also in the temporal dead
zone from the start of its block until its declaration
is evaluated.
if (true) {
let a = 10;
const b = 20;
var c = 30;
}
console.log(c); // Output: 30 (var is NOT block-scoped)
console.log(a); // ReferenceError: a is not defined
console.log(b); // ReferenceError: b is not defined
Here is another example using a for
loop:
for (let i = 0; i < 3; i++) {
console.log(i); // Output: 0, 1, 2
}
console.log(i); // ReferenceError: i is not defined
// Compare with var
for (var j = 0; j < 3; j++) {
console.log(j); // Output: 0, 1, 2
}
console.log(j); // Output: 3 (var is NOT block-scoped)
In loops, let and const stay
inside the loop block, but var is
function-scoped and can still be accessed after the
loop ends. A let loop variable gets a
separate binding for each iteration, which is useful
for callbacks that run later.
Stick with let and const
over var whenever you can. Block
scoping keeps your code predictable and much
easier to debug.
Tip: If callbacks in a loop all log
the same value, check if you used var.
In most cases, let is the better choice.
Best practice: Prefer let and const for predictable block scope. Use var only when you are dealing with legacy code.
- Scope determines where a name can be resolved and which bindings the code can access.
- Scoping is the process the engine uses to look up variables.
- Lexical scoping means scope is determined by where the code is written, not where it is called.
- Global scope: bindings available across the relevant script or environment. In modules, top-level bindings belong to module scope.
- Function scope: the scope created by a function. A nested function can access parent bindings through the scope chain and closures.
-
Block scope: bindings declared
with
letorconstinside a block ({}), accessible only within that block. -
varis function-scoped, whileletandconstare block-scoped. Preferletandconstin modern JavaScript.
Next step: Apply these scope rules while learning how to define and reuse code in the next tutorial on functions.
Videos for this topic will be added soon.