Understanding JavaScript Closures
by Kuligaposten 2026-08-19
A closure is just a function that remembers the scope it was created in — here is why that matters.
Understanding JavaScript Closures
A closure happens whenever a function is defined inside another function and keeps access to the outer function's variables, even after the outer function has finished running. It sounds abstract until you see it — then it's obvious you've been using them all along.
The minimal example
function makeCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
makeCounter() returns, but the inner function still has access to count. Each call to counter() reads and updates the same count variable — it isn't reset, because the closure keeps it alive.
Every call creates a new closure
const counterA = makeCounter();
const counterB = makeCounter();
console.log(counterA()); // 1
console.log(counterA()); // 2
console.log(counterB()); // 1 — a separate count, unaffected by counterA
counterA and counterB each close over their own count, because makeCounter() created a fresh variable on each call.
The classic loop bug
This is the closure gotcha almost everyone hits at least once:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Logs: 3, 3, 3
var is function-scoped, so all three callbacks close over the same i — by the time the timeouts fire, the loop has already finished and i is 3. Switching to let fixes it, because let creates a new binding for i on every iteration:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Logs: 0, 1, 2
Practical use: private state
Closures are how you get private variables in JavaScript without a class:
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(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(100);
account.deposit(50);
console.log(account.getBalance()); // 150
There's no way to reach balance from outside — the only access is through the methods the closure exposes. That's real encapsulation, not just a naming convention like _balance.
Practical use: memoization
function memoize(fn) {
const cache = new Map();
return (arg) => {
if (cache.has(arg)) return cache.get(arg);
const result = fn(arg);
cache.set(arg, result);
return result;
};
}
const slowSquare = (n) => {
for (let i = 0; i < 1e8; i++); // pretend this is expensive
return n * n;
};
const fastSquare = memoize(slowSquare);
fastSquare(5); // computed
fastSquare(5); // returned from cache
The cache map lives only inside the closure memoize returns — every memoized function gets its own private cache, with zero risk of collisions between different memoized functions.