In the previous part, we learned that a closure is simply:
A function that remembers the variables from the environment where it was created.
Now let's move beyond the basics and see how professional JavaScript developers use closures every day.
Real-World Closures
If you've written JavaScript for a while, you've already used closures—even if you didn't realize it.
Closures appear in:
- Event Listeners
- Timers (
setTimeout,setInterval) - Promises
- Async/Await
- React Hooks
- Express Middleware
- Module Pattern
- Factory Functions
- Memoization
- Functional Programming
- Currying
- Dependency Injection
Closures are one of the foundations of modern JavaScript.
Closures in Event Listeners
Suppose we have a button.
<button id="save">Save</button>
Now imagine this JavaScript:
function setupButton() {
let clickCount = 0;
const button = document.getElementById("save");
button.addEventListener("click", function () {
clickCount++;
console.log(`Clicked ${clickCount} times`);
});
}
setupButton();
Every click increases the counter.
Why?
Because the callback remembers clickCount.
Even after setupButton() has already finished executing.
Memory looks like this:
Event Listener
↓
Closure
↓
clickCount
Without closures, every click would start from zero.
Closures with setTimeout()
One of the easiest ways to see closures is with timers.
function delayedGreeting() {
let name = "John";
setTimeout(function () {
console.log(`Hello ${name}`);
}, 3000);
}
delayedGreeting();
Output (after 3 seconds)
Hello John
Question:
How does name still exist?
Because the callback function carries a closure.
Even though delayedGreeting() already finished, JavaScript keeps the variable alive.
Closures Inside Loops
One of the most famous interview questions.
Consider this code:
for (var i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
Output
6
6
6
6
6
Most beginners expect:
1
2
3
4
5
Why?
Because every callback shares the same variable i.
After the loop ends:
i = 6
Every closure references that same variable.
Fix #1 — Using let
for (let i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
Output
1
2
3
4
5
Each iteration creates a brand-new variable.
Each closure gets its own copy.
Fix #2 — Using a Closure
Before let existed, developers solved it like this:
for (var i = 1; i <= 5; i++) {
(function (currentValue) {
setTimeout(function () {
console.log(currentValue);
}, 1000);
})(i);
}
Each Immediately Invoked Function Expression (IIFE) creates a new closure.
Each callback remembers its own value.
Closures in Factory Functions
Factory functions create objects.
Closures allow every object to have private state.
function createUser(name) {
return {
greet() {
console.log(`Hello ${name}`);
}
};
}
const john = createUser("John");
john.greet();
Output
Hello John
Notice that name is completely private.
No one can access it directly.
console.log(john.name);
Output
undefined
Only the closure can access it.
Building a Counter
One of the classic closure examples.
function createCounter() {
let count = 0;
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
reset() {
count = 0;
}
};
}
const counter = createCounter();
console.log(counter.increment());
console.log(counter.increment());
console.log(counter.decrement());
counter.reset();
console.log(counter.increment());
Output
1
2
1
1
Notice something important.
There is no way to modify count directly.
Everything must go through the provided methods.
This is true data encapsulation.
Closures and Private Variables
JavaScript didn't originally have private fields.
Developers used closures instead.
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
balance += amount;
},
withdraw(amount) {
if (amount <= balance) {
balance -= amount;
}
},
getBalance() {
return balance;
}
};
}
Usage
const account = createBankAccount(1000);
account.deposit(500);
console.log(account.getBalance());
Output
1500
Trying this:
console.log(account.balance);
Output
undefined
Balance is truly private.
Closures in Express Middleware
Express middleware relies heavily on closures.
function logger(prefix) {
return function (req, res, next) {
console.log(prefix, req.method, req.url);
next();
};
}
app.use(logger("[API]"));
Here:
prefix
is preserved by the closure.
Every request has access to it.
Closures in React
React Hooks are built around closures.
Example:
function Counter() {
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1);
}
}
The function remembers the variables from its render.
This explains why stale closures sometimes happen in React.
Example:
setTimeout(() => {
console.log(count);
}, 5000);
If count changes before five seconds pass...
The callback may still reference the old value.
Understanding closures makes React much easier to understand.
The Once Function
Imagine an expensive initialization process.
You only want it to run once.
function once(fn) {
let hasRun = false;
let result;
return function (...args) {
if (!hasRun) {
result = fn(...args);
hasRun = true;
}
return result;
};
}
Usage
const initialize = once(function () {
console.log("Initializing...");
return "Application Ready";
});
console.log(initialize());
console.log(initialize());
console.log(initialize());
Output
Initializing...
Application Ready
Application Ready
Application Ready
Initialization happened exactly one time.
Why Does Once Work?
Memory inside the closure
hasRun = false
result = undefined
First call
↓
Run function
↓
Store result
↓
hasRun = true
Next calls
Skip execution
↓
Return stored result
No global variables.
Everything lives inside the closure.
Building a Memoize Function
Memoization avoids repeating expensive computations.
function memoize(fn) {
const cache = {};
return function (...args) {
const key = JSON.stringify(args);
if (cache[key]) {
console.log("Cache Hit");
return cache[key];
}
console.log("Calculating...");
const result = fn(...args);
cache[key] = result;
return result;
};
}
Usage
const square = memoize(function (number) {
return number * number;
});
console.log(square(5));
console.log(square(5));
console.log(square(10));
console.log(square(10));
Output
Calculating...
25
Cache Hit
25
Calculating...
100
Cache Hit
100
The cache survives because of the closure.
Without closures, memoization would be impossible.
Module Pattern
Before ES Modules existed, developers used closures for encapsulation.
const UserModule = (function () {
let users = [];
function addUser(name) {
users.push(name);
}
function getUsers() {
return [...users];
}
return {
addUser,
getUsers
};
})();
Usage
UserModule.addUser("John");
UserModule.addUser("Alice");
console.log(UserModule.getUsers());
Output
["John", "Alice"]
The internal array cannot be accessed directly.
Everything is hidden inside the closure.
Why Professional Developers Love Closures
Closures provide:
- Data Privacy
- Encapsulation
- State Management
- Function Factories
- Memoization
- Dependency Injection
- Functional Programming Patterns
- Cleaner APIs
- Better Reusability
Many popular JavaScript libraries use closures internally.
If closures disappeared from JavaScript today, React, Vue, Express middleware, Redux utilities, and many modern patterns would become much harder—or impossible—to implement in their current form.

Top comments (0)