Closures are the reason some JavaScript code feels “smart” and stateful even without classes or global variables. They let a function quietly carry its own little backpack of data wherever it goes.
The moment closures “clicked”
The first time closures really made sense to many developers was while building a simple counter.
The requirement sounded trivial: “Make a function that increases a number every time it’s called, but don’t let anyone else touch the number.”
The naive solution was a global:
let count = 0;
function increment() {
count++;
return count;
}
It worked until another piece of code also started using the count variable for something else. Suddenly the counter was jumping in strange ways and debugging turned into a hunt for who touched the global.
A closure fixes this elegantly by letting the counter live inside a function, invisible to the outside world yet preserved between calls.
What a closure really is
In simple language:
A closure is a function that keeps access to the variables around it, even after the outer function has finished running.
JavaScript doesn’t throw away the surrounding variables as long as an inner function still needs them.
That’s why a “returned function” can still see variables from the place where it was created.
A practical counter with closures
Here’s a counter that proves the idea:
function createCounter(start = 0) {
let count = start;
return function next() {
count += 1;
return count;
};
}
const counterA = createCounter();
console.log(counterA()); // 1
console.log(counterA()); // 2
const counterB = createCounter(10);
console.log(counterB()); // 11
console.log(counterA()); // 3 (independent from B)
Key observations:
- count is not global; it lives inside createCounter.
- Each call to createCounter gets its own count.
- The inner next function still “remembers” count even though createCounter already finished.
Closures with multiple values
Closures are not limited to a single variable.
A common pattern is to capture both configuration and state:
function createAdder(base, step = 1) {
let total = base;
return function add() {
total += step;
return total;
};
}
const addFive = createAdder(0, 5);
console.log(addFive()); // 5
console.log(addFive()); // 10
const addTenFrom100 = createAdder(100, 10);
console.log(addTenFrom100()); // 110
Each returned function closes over base, step, and total.
Different instances share the same behavior but maintain their own state.
Lexical scope: why closures work at all
JavaScript uses lexical scoping, meaning a function’s accessible variables are determined by where the function is written in the code, not where it is called.
function outer() {
const message = "Hello from outer";
function inner() {
console.log(message);
}
return inner;
}
const fn = outer();
fn(); // logs: "Hello from outer"
Even if fn is called in a totally different place, it still sees message because it was defined inside outer.
The combination of lexical scoping + the fact that JS keeps those variables alive for inner functions = closures.
Private variables with closures
Closures are a simple way to simulate “private fields” without classes.
function createSecret() {
let value = 0;
function increment() {
value += 1;
}
function get() {
return value;
}
return { increment, get };
}
const secret = createSecret();
secret.increment();
secret.increment();
console.log(secret.get()); // 2
// value is not accessible directly here
There is no way to change value from outside except through increment.
This gives you controlled state changes and makes bugs easier to track.
Closures in loops: classic trap and modern fix
A very common bug appears when using closures inside loops.
Old code often looked like this:
var buttons = [];
for (var i = 0; i < 3; i++) {
buttons[i] = function () {
console.log("Button index:", i);
};
}
buttons[0](); // 3
buttons[1](); // 3
buttons[2](); // 3
All handlers log 3 because var shares one i for the entire loop, and by the time the functions run, the loop is finished.
Two better approaches:
1. Use let (new scope per iteration)
for (let i = 0; i < 3; i++) {
buttons[i] = function () {
console.log("Button index:", i);
};
}
Now each closure captures its own i.
2. Wrap in a factory function
for (var i = 0; i < 3; i++) {
(function (index) {
buttons[index] = function () {
console.log("Button index:", index);
};
})(i);
}
Both approaches rely on closures; let just makes it much easier.
Closures with timers: remembering values in async code
Closures really shine when combined with setTimeout or setInterval, where code runs later but still needs access to earlier variables.
function startCounter(delayMs = 1000) {
let count = 0;
setInterval(() => {
count += 1;
console.log("Tick:", count);
}, delayMs);
}
startCounter(); // prints 1, 2, 3, ...
The arrow function passed to setInterval captures the count variable.
Even though startCounter has finished, the closure keeps count alive and updated on every tick.
Another classic pattern is preserving the index in timed loops:
for (let i = 1; i <= 3; i++) {
setTimeout(() => {
console.log("Timeout index:", i);
}, i * 500);
}
Thanks to closures + let, each timeout prints the right number.
When to reach for closures in your own code
Closures are not just a theory topic; they naturally appear whenever you:
- Need state that lives across calls but should stay hidden.
- Build function factories (functions that return customized functions).
- Write event handlers, timers, or callbacks that must remember where they came from.
- Implement counters, cache layers, or simple modules without global variables.
If a variable should belong tightly to a function and not leak into the rest of the codebase, a closure is usually the cleanest choice.
