🎒 Understanding Closures
A closure is a function that remembers the variables around it, even after the code that created them has finished running. It sounds abstract, but it is the quiet engine behind private data, React Hooks, and half the clever patterns you will meet in real JavaScript.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Define a closure and explain how lexical scoping and the scope chain make it possible
- Use closures to build private state, function factories, and memoized functions
- Diagnose and fix the two classic gotchas: loop variables and the
thiskeyword - Recognize closures at work in React Hooks, event handlers, and Express middleware
Estimated Time: 35–45 minutes • Difficulty: Intermediate
Hands-on: Build a counter factory and a tiny event emitter that rely on closures for state.
In This Lesson
What Is a Closure?
A closure is the combination of a function together with the lexical environment in which it was declared. In plainer words: whenever you create a function, it quietly keeps a reference to the variables that were in scope where it was written — and it can still reach them later, even if the outer function has already returned.
🎒 The backpack analogy: Picture every function packing a backpack the moment it is created. Into that backpack go all the variables it could see at birth. Wherever the function travels afterwards — into an array, an event listener, a network callback — it carries the backpack with it and can always open it.
Here is the smallest example that shows the effect. Watch how inner still reads message long after outer has finished:
function outer() {
const message = 'I was captured at creation time';
function inner() {
console.log(message); // reaches into outer's scope
}
return inner; // outer returns and exits here
}
const remembered = outer();
remembered(); // "I was captured at creation time"
By the time remembered() runs, outer() has already returned. Normally its local variable message would be gone. But because inner closed over it, the variable lives on for as long as inner does. That is the whole idea — everything else in this lesson is a consequence of it.
message survives.How Closures Work
Closures fall out of three features working together. None of them is exotic; you have been using all three already.
1. Lexical scoping
JavaScript uses lexical (static) scoping: a function's scope is decided by where it is written in the source, not by where or when it is called. An inner function can always see the variables of the functions that physically enclose it.
const globalVar = 'global';
function outer() {
const outerVar = 'outer';
function inner() {
const innerVar = 'inner';
console.log(innerVar); // own scope
console.log(outerVar); // parent scope
console.log(globalVar); // global scope
}
inner();
}
2. The scope chain
When JavaScript looks up a name, it checks the current scope first, then the enclosing scope, then the next one out, all the way to the global scope. This ordered search is the scope chain.
3. Variables outlive their function
Normally a function's local variables are discarded when it returns. But if a closure still references them, the garbage collector leaves them alone. The count below is created once and then shared across every call:
function createCounter() {
let count = 0; // created once, preserved by the closure
return function () {
count += 1;
return count;
};
}
const next = createCounter();
console.log(next()); // 1
console.log(next()); // 2
console.log(next()); // 3
📖 Key Terms
Lexical environment: the set of variables in scope at the place a function is defined.
Scope chain: the ordered list of environments JavaScript searches when resolving a name.
Free variable: a variable a function uses but does not itself declare — exactly what a closure captures.
Private State & Function Factories
The most valuable everyday use of closures is encapsulation: keeping data private and exposing only the operations you choose. Because balance below lives inside the closure, nothing outside can read or corrupt it directly.
function createBankAccount(initialBalance) {
let balance = initialBalance; // private — no outside access
return {
deposit(amount) {
if (amount <= 0) return 'Invalid deposit amount';
balance += amount;
return `Deposited ${amount}. New balance: ${balance}`;
},
withdraw(amount) {
if (amount <= 0 || amount > balance) return 'Invalid withdrawal amount';
balance -= amount;
return `Withdrew ${amount}. New balance: ${balance}`;
},
getBalance() {
return balance;
},
};
}
const account = createBankAccount(100);
console.log(account.getBalance()); // 100
console.log(account.deposit(50)); // "Deposited 50. New balance: 150"
console.log(account.withdraw(30)); // "Withdrew 30. New balance: 120"
console.log(account.balance); // undefined — genuinely private
Function factories
A factory is a function that returns customized functions. Each returned function closes over the arguments it was built with, so you can stamp out specialized behavior on demand:
function createGreeter(greeting) {
return (name) => `${greeting}, ${name}!`;
}
const sayHello = createGreeter('Hello');
const sayHowdy = createGreeter('Howdy');
console.log(sayHello('Alice')); // "Hello, Alice!"
console.log(sayHowdy('Bob')); // "Howdy, Bob!"
Memoization: closures for speed
A closure can hold a private cache that survives between calls, letting an expensive function skip work it has already done:
function memoize(fn) {
const cache = new Map(); // private, persists across calls
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn(...args);
cache.set(key, result);
return result;
};
}
const slowSquare = (n) => {
for (let i = 0; i < 1e7; i++) {} // pretend this is expensive
return n * n;
};
const fastSquare = memoize(slowSquare);
console.log(fastSquare(9)); // computed once
console.log(fastSquare(9)); // returned instantly from cache
✅ The pattern to notice
In all three examples the shape is identical: an outer function declares some state, then returns an inner function (or object of functions) that reads and updates that state. The state is invisible from outside — that is encapsulation, achieved with nothing but a closure.
The Module Pattern
Before ES modules existed, developers used an Immediately Invoked Function Expression (IIFE) to create a private scope and expose a public interface. This "module pattern" is closures at scale, and you will still meet it in older codebases and library builds.
const calculator = (function () {
// private
let result = 0;
const isValid = (n) => typeof n === 'number' && !Number.isNaN(n);
// public interface
return {
add(n) { if (isValid(n)) result += n; return this; },
subtract(n) { if (isValid(n)) result -= n; return this; },
getResult() { return result; },
reset() { result = 0; return this; },
};
})();
calculator.add(5).subtract(2).add(10);
console.log(calculator.getResult()); // 13
The IIFE runs once, its local result and isValid never leak into the global scope, and the returned object's methods keep working because they closed over them. Returning this from each method is what enables the fluent method chaining you see above.
💡 From module pattern to ES modules
Today an import/export file gives you the same privacy for free — anything you do not export stays private to the module. The mental model, though, is the closure you just wrote by hand.
Common Gotchas
1. Loop variables with var
The most famous closure bug: create functions inside a loop with var, and every one of them shares the same variable. By the time they run, the loop has finished and the variable holds its final value.
// Bug: all three log 3
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 3, 3, 3
// Fix: let is block-scoped — each iteration gets its own i
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 0, 1, 2
⚠️ Why let fixes it
With var there is one i for the whole loop, so all closures capture the same box. With let, JavaScript creates a fresh binding of i for every iteration, so each closure captures its own value. Before ES6, developers wrapped the body in an IIFE — let made that ceremony obsolete.
2. The this keyword
this is not captured by an ordinary closure — a regular inner function gets its own this, which is usually undefined in strict mode. Arrow functions are different: they have no this of their own and inherit it lexically, which is exactly what you want here.
const user = {
name: 'Alice',
greetLater() {
// Arrow function inherits `this` from greetLater
setTimeout(() => {
console.log(`Hi, I'm ${this.name}`);
}, 100);
},
};
user.greetLater(); // "Hi, I'm Alice"
// A regular function would have lost `this`:
// setTimeout(function () { console.log(this.name); }, 100); // undefined
3. Accidental memory retention
A closure keeps its captured variables alive. If it captures something large and lives a long time (say, an event listener that is never removed), that memory cannot be freed. The cure is to release the reference when you are done.
function makeProcessor() {
const big = new Array(1_000_000).fill('data');
return (i) => big[i];
}
let read = makeProcessor(); // `big` is retained by the closure
// ...use it...
read = null; // now `big` can be garbage collected
Closures in Modern JavaScript
Closures did not fade away when classes and modules arrived — they moved under the surface of the tools you use every day.
React Hooks are closures
useState works because the setter and the value close over a slot that persists between renders. A stripped-down mental model:
// Greatly simplified — the real React tracks slots per component
function useState(initialValue) {
let value = initialValue;
const setValue = (next) => {
value = next;
rerender(); // schedule a re-render
};
return [value, setValue];
}
Express middleware factories
Configurable middleware is a function factory: options are captured in a closure, and the returned middleware reads them on every request.
function requireRole(role) {
// `role` is captured for the life of this middleware
return function (req, res, next) {
if (req.user?.role !== role) {
return res.status(403).json({ message: 'Insufficient permissions' });
}
next();
};
}
app.get('/admin', requireRole('admin'), (req, res) => {
res.json({ message: 'Admin dashboard' });
});
💡 Private class fields vs. closures
Modern classes offer real privacy with the # prefix (#count). It is a great option for object-oriented code, but closures remain the go-to for standalone functions, callbacks, and anywhere you are not writing a class.
Hands-on Exercise
🏋️ Build a Counter Factory and a Tiny Event Emitter
Objective: Prove to yourself that closures preserve independent, private state.
Instructions:
- Write
makeCounter(start = 0)that returns an object withincrement(),decrement(), andvalue(). Each counter must keep its own count. - Create two counters and confirm that operating on one never affects the other.
- Write
createEmitter()that returnson(event, fn)andemit(event, ...args), storing listeners in a private object captured by the closure. - Register two listeners for a
'ping'event and emit it once — both should fire.
💡 Hint
For the counter, declare let count = start; inside makeCounter and have each returned method read and update it. For the emitter, keep const listeners = {}; and push functions into listeners[event].
✅ Sample solution
function makeCounter(start = 0) {
let count = start; // private state per counter
return {
increment() { return ++count; },
decrement() { return --count; },
value() { return count; },
};
}
const a = makeCounter();
const b = makeCounter(100);
a.increment(); // 1
b.increment(); // 101
console.log(a.value(), b.value()); // 1 101 — independent
function createEmitter() {
const listeners = {}; // private registry
return {
on(event, fn) {
(listeners[event] ??= []).push(fn);
},
emit(event, ...args) {
(listeners[event] || []).forEach((fn) => fn(...args));
},
};
}
const bus = createEmitter();
bus.on('ping', (msg) => console.log('A got', msg));
bus.on('ping', (msg) => console.log('B got', msg));
bus.emit('ping', 'hello');
// A got hello
// B got hello
Best Practices
✅ Do
- Reach for closures to keep state private instead of using shared globals.
- Use
let/constin loops so each iteration captures its own binding. - Use arrow functions when you need an inner function to inherit
this. - Null out or remove long-lived closures (event listeners, timers) when you are done with them.
⚠️ Don't
- Don't capture large data structures in closures that outlive their usefulness — that is a memory leak.
- Don't expect a regular inner function to inherit the outer
this; it won't. - Don't reuse a single
varacross loop iterations and expect per-iteration values. - Don't hide so much behind closures that debugging becomes guesswork — keep captured state small and named clearly.
Summary & Quiz
🎉 Key Takeaways
- A closure is a function plus the lexical environment it captured at creation.
- Closures let variables outlive the function that created them, enabling private state.
- They power function factories, memoization, and the module pattern.
- The classic bugs are loop variables with
var(fix withlet) andthis(fix with arrow functions). - React Hooks and Express middleware are closures in disguise.
🎯 Quick Quiz
Question 1: What exactly does a closure "close over"?
Question 2: Why do setTimeout callbacks in a var loop all print the final value?
Question 3: Which technique keeps a bank balance truly private in plain JavaScript?
📚 Further Reading
🚀 What's Next?
Closures are the foundation for treating functions as values you can pass and return. Next we build directly on them to explore higher-order function patterns — map, compose, curry, decorators, and more.
🎉 Nicely done!
You now understand the backpack every JavaScript function carries. Let's put it to work.