Skip to main content

Scope and Closures

What is scope?

Scope determines where variables can be accessed. If a variable is outside the current scope, your code cannot use it.

function greet() {
const message = "Hello";
console.log(message);
}

greet(); // "Hello"

// This would cause an error:
// console.log(message); // ReferenceError

The variable message exists only inside the greet function.

Why this matters

Scope helps you keep code organized and prevents names from colliding. It also explains why some variables are available in one part of a program but not another.

Closures build on scope. They let functions remember variables from the place where they were created, which is one of JavaScript's most important function patterns.

Global scope

A variable declared outside functions and blocks is in global scope:

const appName = "Todo App";

function printAppName() {
console.log(appName);
}

printAppName(); // "Todo App"

Global variables can be read from many places, but too many globals make programs harder to reason about. Prefer keeping variables in the smallest scope that needs them.

Function scope

Variables declared inside a function are available only inside that function:

function createUser() {
const name = "Alice";
return { name };
}

console.log(createUser()); // { name: "Alice" }

// This would cause an error:
// console.log(name); // ReferenceError

Each function call gets its own local variables:

function addOne(number) {
const result = number + 1;
return result;
}

console.log(addOne(1)); // 2
console.log(addOne(5)); // 6

Block scope

Variables declared with let and const are block-scoped. A block is code inside {}.

if (true) {
const message = "Inside block";
console.log(message);
}

// This would cause an error:
// console.log(message); // ReferenceError

Output:

Inside block

Blocks include:

  • if statements
  • for loops
  • while loops
  • switch statements
  • standalone {} blocks

Loop variables declared with let are also block-scoped:

for (let i = 0; i < 3; i++) {
console.log(i);
}

// This would cause an error:
// console.log(i); // ReferenceError

Output:

0
1
2

var has function scope

Remember from the variables guide that var has older scoping behavior. It is function-scoped, not block-scoped:

if (true) {
var functionScoped = "I leak out of the block";
const blockScoped = "I stay in the block";
}

console.log(functionScoped); // "I leak out of the block"

// This would cause an error:
// console.log(blockScoped); // ReferenceError

This is one reason modern JavaScript uses const and let instead of var.

Variable hoisting

Hoisting means JavaScript sets up declarations before code runs. Variables declared with var, let, and const are all hoisted in a technical sense, but they behave differently.

With var, the variable exists before the declaration line and starts as undefined:

console.log(count); // undefined

var count = 1;

With let and const, the variable cannot be used before the declaration line:

// This would cause an error:
// console.log(count); // ReferenceError

const count = 1;

Rule of thumb: Declare variables before using them, and prefer const or let.

Function hoisting is covered in the functions guide.

Variable shadowing

Shadowing happens when an inner scope has a variable with the same name as an outer scope:

const name = "Alice";

function greet() {
const name = "Bob";
console.log(name);
}

greet(); // "Bob"
console.log(name); // "Alice"

The inner name hides the outer name while inside the function.

Shadowing is allowed, but it can be confusing. Use more specific names when clarity matters:

const userName = "Alice";

function greet() {
const greetingName = "Bob";
console.log(greetingName);
}

greet(); // "Bob"

Lexical scope

JavaScript uses lexical scope, which means scope is based on where code is written, not where a function is called.

const name = "Alice";

function outer() {
const name = "Bob";

function inner() {
console.log(name);
}

return inner;
}

const innerFunction = outer();
innerFunction(); // "Bob"

inner uses the name from outer because that is where inner was created.

Scope chain

When JavaScript looks for a variable, it checks scopes from inside to outside:

  1. Current scope
  2. Outer scope
  3. Next outer scope
  4. Global scope
  5. If not found, ReferenceError
const globalValue = "global";

function outer() {
const outerValue = "outer";

function inner() {
const innerValue = "inner";

console.log(innerValue); // "inner"
console.log(outerValue); // "outer"
console.log(globalValue); // "global"
}

inner();
}

outer();

This chain is why inner functions can use variables from outer functions.

What are closures?

A closure is a function that remembers variables from its outer scope, even after that outer function has finished running.

function createGreeting(name) {
return function() {
return `Hello, ${name}!`;
};
}

const greetAlice = createGreeting("Alice");

console.log(greetAlice()); // "Hello, Alice!"

The returned function remembers name, even though createGreeting has already finished.

Closures for private state

Closures can keep state private:

function createCounter() {
let count = 0;

return function() {
count += 1;
return count;
};
}

const counter = createCounter();

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

Only the returned function can access and update count.

Closures in loops

Closures can be surprising in loops when var is used:

for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}

Output:

3
3
3

All callbacks share the same function-scoped i.

Use let to create a new binding for each loop iteration:

for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}

Output:

0
1
2

This is one of the clearest practical reasons to avoid var.

This example uses timer callbacks. Callbacks are covered in more detail in the callback functions guide.

Closures remember variables

Closures remember variables from outer scopes, not just the value the variable had at one moment.

function createCounter() {
let count = 0;

return {
increment() {
count += 1;
},
getCount() {
return count;
}
};
}

const counter = createCounter();

counter.increment();
counter.increment();

console.log(counter.getCount()); // 2

Both methods close over the same count variable.

When increment() changes count, getCount() sees the updated value.

Each function call creates a new scope

Each call to a function creates a new set of local variables.

That means each closure can remember its own state.

function createCounter() {
let count = 0;

return function() {
count += 1;
return count;
};
}

const firstCounter = createCounter();
const secondCounter = createCounter();

console.log(firstCounter()); // 1
console.log(firstCounter()); // 2
console.log(secondCounter()); // 1

firstCounter and secondCounter each remember a different count variable.

Common patterns

Keeping helper variables local

function formatName(firstName, lastName) {
const trimmedFirst = firstName.trim();
const trimmedLast = lastName.trim();

return `${trimmedFirst} ${trimmedLast}`;
}

The temporary variables stay inside the function where they are needed.

Creating specialized functions

function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

Each returned function remembers its own multiplier.

Avoiding accidental globals

Always declare variables with const or let:

function saveUser(user) {
const normalizedName = user.name.trim();
return normalizedName;
}

Avoid assigning to a name that was never declared:

function saveUser(user) {
// Avoid this:
// normalizedName = user.name.trim();
}

Best practices

  • Use the smallest useful scope: Declare variables where they are needed.
  • Prefer const and let: Avoid var in new code.
  • Declare before use: Do not rely on hoisting.
  • Avoid unnecessary globals: They are easy to accidentally reuse or overwrite.
  • Avoid confusing shadowing: Use clearer names when nested scopes overlap.
  • Use closures intentionally: They are useful for private state and specialized functions.
  • Remember that closures keep variables alive: If two functions close over the same variable, they see the same changing value.
  • Use let in loops when callbacks need the current loop value.

Summary

Scope controls where variables are available. JavaScript has global scope, function scope, and block scope. The scope chain lets inner functions access outer variables, and closures let functions remember those variables even after the outer function has finished. Closures remember variables, not frozen copies of values. Prefer const and let, keep variables in the smallest useful scope, and avoid relying on hoisting.