Closures
Learn what closures are, how they work, and why they are one of the most powerful features in JavaScript.
Closures
Closures are one of those concepts that sounds complicated but becomes completely natural once it clicks. And once it clicks — you realize you have been using closures all along without knowing it.
A closure is a function that remembers the variables from the scope where it was created — even after that scope has finished executing.
function outer() {
const message = "Hello from outer!";
function inner() {
console.log(message); // accesses outer's variable
}
return inner;
}
const fn = outer();
fn(); // Hello from outer!outer has finished running. Its scope is gone. But inner still has access to message. That is a closure — inner closed over the variable message and kept it alive.
How Closures Work
To understand closures you need to remember two things from the Scope topic:
- Functions have access to variables in their outer scope — lexical scoping
- Scope is determined by where the function is written — not where it is called
When a function is created, JavaScript attaches a reference to its surrounding scope — called the lexical environment. This reference stays attached no matter where the function goes.
When inner is returned and called later, it looks up message through its attached reference to outer's scope — and finds it still there.
A Simple Closure
function makeCounter() {
let count = 0; // this variable is closed over
return function() {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3makeCounter runs once and returns a function. That returned function closes over count. Every time you call counter(), it increments and returns the same count — because it is always referring to the same variable in the closed-over scope.
Multiple counters are independent
const counter1 = makeCounter();
const counter2 = makeCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1 — independent, own count
console.log(counter1()); // 3
console.log(counter2()); // 2Each call to makeCounter() creates a new scope with its own count. counter1 and counter2 each close over a different count — completely independent.
Closures Capture Variables — Not Values
This is the most important thing to understand about closures. They capture the variable itself — not the value it holds at the time of creation.
function makeAdder(x) {
return function(y) {
return x + y; // x is captured — whatever x is at call time
};
}
const add5 = makeAdder(5);
const add10 = makeAdder(10);
console.log(add5(3)); // 8 — x is 5
console.log(add10(3)); // 13 — x is 10// Closures capture the variable — so changes are visible
function makeCounter() {
let count = 0;
return {
increment() { count++; },
decrement() { count--; },
getCount() { return count; }
};
}
const counter = makeCounter();
counter.increment();
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.getCount()); // 2All three methods close over the same count variable — they share it. Changes made by one method are visible to the others.
The Classic Loop Closure Bug
This is probably the most famous closure gotcha in JavaScript.
// ❌ Classic bug with var
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // what does this print?
}, 100);
}
// 3
// 3
// 3Expected 0, 1, 2 — got 3, 3, 3. Why?
var is function scoped — there is only one i shared across all iterations. All three setTimeout callbacks close over the same i. By the time they run, the loop has finished and i is 3.
Fix 1 — use let
// ✅ let creates a new binding per iteration
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 0, 1, 2
}, 100);
}let creates a new i for each iteration. Each callback closes over its own i.
Fix 2 — use an IIFE to capture the value
// ✅ IIFE creates a new scope per iteration
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => {
console.log(j); // 0, 1, 2
}, 100);
})(i);
}The IIFE (Immediately Invoked Function Expression) creates a new scope and captures the current value of i as j. This was the standard fix before let existed.
Practical Uses of Closures
1. Data privacy — module pattern
Closures let you create private variables that cannot be accessed from outside.
function createBankAccount(initialBalance) {
let balance = initialBalance; // private — not accessible from outside
return {
deposit(amount) {
if (amount <= 0) throw new Error("Invalid amount");
balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) throw new Error("Insufficient funds");
balance -= amount;
return balance;
},
getBalance() {
return balance;
}
};
}
const account = createBankAccount(1000);
console.log(account.deposit(500)); // 1500
console.log(account.withdraw(200)); // 1300
console.log(account.getBalance()); // 1300
console.log(account.balance); // undefined — privatebalance is completely private. The only way to interact with it is through the returned methods — which all close over it.
2. Function factories
Create specialized functions from a general one.
function createMultiplier(multiplier) {
return (number) => number * multiplier;
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const tenX = createMultiplier(10);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(tenX(5)); // 50
// Real use — tax calculators for different regions
function createTaxCalculator(taxRate) {
return (price) => price + price * taxRate;
}
const pakistanTax = createTaxCalculator(0.17);
const ukTax = createTaxCalculator(0.20);
console.log(pakistanTax(1000)); // 1170
console.log(ukTax(1000)); // 12003. Memoization — caching results
function memoize(fn) {
const cache = new Map(); // closed over — persists between calls
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const fastFib = memoize(fibonacci);
console.log(fastFib(40)); // computed once
console.log(fastFib(40)); // instant — from cacheThe cache Map lives in the closure — it persists between calls to the memoized function but is not accessible from outside.
4. Event handlers with state
function createButton(label) {
let clickCount = 0; // private state per button
const btn = document.createElement("button");
btn.textContent = label;
btn.addEventListener("click", () => {
clickCount++;
console.log(`${label} clicked ${clickCount} times`);
btn.textContent = `${label} (${clickCount})`;
});
return btn;
}
const likeBtn = createButton("Like");
const shareBtn = createButton("Share");
document.body.appendChild(likeBtn);
document.body.appendChild(shareBtn);
// Each button has its own clickCount — completely independent5. Partial application
Lock in some arguments now and supply the rest later.
function partial(fn, ...presetArgs) {
return function(...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
function log(level, timestamp, message) {
console.log(`[${level}] ${timestamp}: ${message}`);
}
const logError = partial(log, "ERROR");
const logInfo = partial(log, "INFO");
logError(new Date().toISOString(), "Something broke");
logInfo(new Date().toISOString(), "Server started");Closures and Memory
Because closures keep the outer scope alive, they can cause memory leaks if not handled carefully.
// ❌ Potential memory issue
function attachHandler(element) {
const largeData = new Array(100000).fill("data"); // large data
element.addEventListener("click", () => {
console.log(largeData[0]); // closure keeps largeData alive
});
}largeData stays in memory as long as the event listener exists — because the callback closes over it. If the element is removed from the DOM but the listener is not removed, the memory leaks.
// ✅ Better — only close over what you need
function attachHandler(element) {
const largeData = new Array(100000).fill("data");
const firstItem = largeData[0]; // only keep what you need
element.addEventListener("click", () => {
console.log(firstItem); // closes over only firstItem — not all of largeData
});
}Close over only what you need. Do not keep large objects alive in closures longer than necessary.
Recognizing Closures
Any time you see a function accessing a variable from an outer scope — that is a closure. They are everywhere.
// All of these are closures
// setTimeout callback
const name = "Ali";
setTimeout(() => console.log(name), 1000); // closes over name
// Array methods
const multiplier = 3;
[1, 2, 3].map(n => n * multiplier); // closes over multiplier
// Event listeners
const btn = document.querySelector("button");
let count = 0;
btn.addEventListener("click", () => count++); // closes over count
// React hooks
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
// onClick closes over count and setCount
}Summary
- A closure is a function that remembers variables from its outer scope even after that scope has finished
- JavaScript attaches a reference to the surrounding lexical environment when a function is created
- Closures capture variables — not values — changes to the variable are visible in the closure
- Each call to a function that returns a closure creates a new independent scope
- The classic loop bug —
varshares one variable across iterations — fix withletor an IIFE - Practical uses — data privacy, function factories, memoization, event handlers with state, partial application
- Be careful with closures and memory — they keep the outer scope alive, which can cause leaks if overused
- Closures are everywhere in JavaScript — every callback, every event handler, every array method you write uses them