What Is a Closure?
A closure is the combination of a function and the lexical environment in which it was declared. In simple terms, it is the ability of a function to remember and access variables from the environment where it was created. When a function references variables from an outer scope, it can still access those variables even after that scope has ended.
Closures are one of the most fundamental concepts in JavaScript, used extensively in data encapsulation, factory patterns, event handlers, React hooks, and more.
Scope Chain and Lexical Environment
When the JavaScript engine executes code, it creates an Execution Context. Each execution context contains a Lexical Environment that stores variables, and through references to outer environments, forms the scope chain.
const globalVar = "global";
function outer() {
const outerVar = "outer";
function inner() {
const innerVar = "inner";
// Scope chain: inner -> outer -> global
console.log(innerVar); // "inner" (own scope)
console.log(outerVar); // "outer" (outer function scope)
console.log(globalVar); // "global" (global scope)
}
inner();
}
outer();
// inner
// outer
// global
When looking up a variable, the search starts from the current scope and moves up to parent scopes. This chain is the scope chain.
| Scope Type | Created When | Example |
|---|---|---|
| Global scope | Program start | const globalVar |
| Function scope | Function call | var, function declarations |
| Block scope | Entering a block {} | let, const declarations |
How Closures Work
A closure remembers the scope where the function was defined, not where it is called.
function createCounter(initial = 0) {
let count = initial; // This variable is preserved by the closure
return {
increment() {
count += 1;
return count;
},
decrement() {
count -= 1;
return count;
},
getCount() {
return count;
},
};
}
const counter = createCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
console.log(counter.getCount()); // 11
// Direct access to count is impossible — encapsulation
// console.log(count); // ReferenceError
Even after createCounter() finishes executing, the count variable is not garbage collected because the returned methods still reference it. This is the core of closures.
Practical Pattern 1 — Data Encapsulation
Closures are used to implement private variables. They create state that cannot be directly accessed from outside.
function createWallet(ownerName, initialBalance = 0) {
// Private variables — cannot be accessed directly from outside
let balance = initialBalance;
const transactions = [];
function recordTransaction(type, amount) {
transactions.push({
type,
amount,
balance,
timestamp: new Date().toISOString(),
});
}
return {
deposit(amount) {
if (amount <= 0) throw new Error("Only positive amounts can be deposited");
balance += amount;
recordTransaction("deposit", amount);
return `Deposited ${amount}. Balance: ${balance}`;
},
withdraw(amount) {
if (amount <= 0) throw new Error("Only positive amounts can be withdrawn");
if (amount > balance) throw new Error("Insufficient balance");
balance -= amount;
recordTransaction("withdrawal", amount);
return `Withdrew ${amount}. Balance: ${balance}`;
},
getBalance() {
return balance;
},
getHistory() {
return [...transactions]; // Return a copy
},
getOwner() {
return ownerName;
},
};
}
const wallet = createWallet("John", 10000);
console.log(wallet.deposit(5000));
// Deposited 5000. Balance: 15000
console.log(wallet.withdraw(3000));
// Withdrew 3000. Balance: 12000
console.log(wallet.getBalance());
// 12000
// Direct access to balance and transactions is impossible
// console.log(wallet.balance); // undefined
Practical Pattern 2 — Function Factory
Closures can create specialized functions that remember configuration values.
// Rate limiter function factory
function createRateLimiter(maxCalls, windowMs) {
const calls = [];
return function rateLimiter() {
const now = Date.now();
// Remove calls outside the time window
while (calls.length > 0 && calls[0] <= now - windowMs) {
calls.shift();
}
if (calls.length >= maxCalls) {
return { allowed: false, retryAfter: calls[0] + windowMs - now };
}
calls.push(now);
return { allowed: true, remaining: maxCalls - calls.length };
};
}
// A limiter that allows 3 calls per second
const limiter = createRateLimiter(3, 1000);
console.log(limiter()); // { allowed: true, remaining: 2 }
console.log(limiter()); // { allowed: true, remaining: 1 }
console.log(limiter()); // { allowed: true, remaining: 0 }
console.log(limiter()); // { allowed: false, retryAfter: ... }
Common Mistake — Closures in Loops
Using var with loops produces unexpected behavior. This is the most famous closure pitfall.
// Wrong: var has function scope
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // All print 3 (the value of i after the loop ends)
}, 100);
}
// 3, 3, 3
// Correct solution 1: let has block scope (new binding per iteration)
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // Correctly prints 0, 1, 2
}, 100);
}
// Correct solution 2: IIFE to create a closure (pre-ES6 approach)
for (var i = 0; i < 3; i++) {
(function (captured) {
setTimeout(() => {
console.log(captured); // 0, 1, 2
}, 100);
})(i);
}
var has function scope, so a single i is shared across the entire loop. let has block scope, creating an independent i for each iteration.
Memory Considerations
Variables referenced by closures are not garbage collected, so be mindful of memory leaks.
function createHeavyClosure() {
// Large data referenced by the closure
const hugeArray = new Array(1000000).fill("data");
return function process() {
// The entire hugeArray remains in memory
return hugeArray.length;
};
}
// Improved: only include necessary data in the closure
function createLightClosure() {
const hugeArray = new Array(1000000).fill("data");
const length = hugeArray.length; // Extract only the needed value
// hugeArray is no longer referenced, eligible for GC
return function process() {
return length;
};
}
Summary
Closures are a core mechanism of JavaScript, and understanding them correctly enables powerful patterns.
- Definition: The combination of a function and the lexical environment in which it was declared
- Scope chain: The chain that searches for variables from the current scope up to parent scopes
- Encapsulation: Implementing private variables to restrict external access
- Factory: Creating specialized functions that remember configuration values
- Caveats: Be aware of the
var+ loop pitfall and memory leaks - Modern usage: Widely used in React hooks, event handlers, currying, memoization, and more