Written by Rahul · Frontend Engineer at Google · Updated 2025
Closures are one of those concepts where once it clicks, you'll see them everywhere. I use closures every single day at Google — in React hooks, in utility functions, in caching layers. Let me explain it the way that finally made it click for me.
What is a Closure?
A closure is a function that remembers the variables from the scope where it was created, even after that scope has finished executing.
function createCounter() {
let count = 0; // This variable is "enclosed"
return function increment() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// "count" should be dead (createCounter finished executing)
// But the returned function still has access to it — that's a closure!Why Closures Exist — The Technical Reason
When a function is created in JavaScript, it gets a hidden property called [[Environment]] that links to the scope where it was born. The garbage collector won't clean up that scope as long as the function exists.
Real Production Use Cases
1. React Hooks — Closures Everywhere
Every single React hook is powered by closures:
function SearchComponent() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
// This callback "closes over" query
const handleSearch = useCallback(() => {
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(data => setResults(data));
}, [query]); // query is a closed-over variable
// The effect closes over handleSearch
useEffect(() => {
const timer = setTimeout(handleSearch, 300);
return () => clearTimeout(timer);
}, [handleSearch]);
}2. Private Variables (Module Pattern)
function createAuthService() {
let token = null; // Private — can't be accessed directly
return {
login(credentials) {
token = authenticate(credentials);
},
getToken() {
return token;
},
isAuthenticated() {
return token !== null;
},
logout() {
token = null;
}
};
}
const auth = createAuthService();
auth.login({ user: "rahul", pass: "***" });
console.log(auth.isAuthenticated()); // true
console.log(auth.token); // undefined — truly private!3. Function Factories
function createMultiplier(factor) {
return (number) => number * factor;
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const toPercentage = createMultiplier(100);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(toPercentage(0.85)); // 854. Memoization / Caching
function memoize(fn) {
const cache = new Map(); // Closed over by the returned function
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log("Cache hit!");
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const expensiveCalc = memoize((n) => {
console.log("Computing...");
return n * n;
});
expensiveCalc(5); // "Computing..." → 25
expensiveCalc(5); // "Cache hit!" → 25The Classic Pitfall — Loop + Closure
// ❌ Classic bug
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3 (not 0, 1, 2!)
// Why? All three functions close over the SAME "i" variable
// By the time setTimeout fires, i is already 3
// ✅ Fix 1: Use let (block scoping)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2
// ✅ Fix 2: IIFE (creates new scope per iteration)
for (var i = 0; i < 3; i++) {
((j) => {
setTimeout(() => console.log(j), 100);
})(i);
}Production Issues I've Encountered
Issue 1: Memory Leak from Closures
function setupHandler() {
const hugeData = new Array(1000000).fill("x"); // 1M items
document.addEventListener("click", () => {
// This closure keeps hugeData alive forever!
console.log(hugeData.length);
});
}
// ✅ Fix: Only close over what you need
function setupHandlerFixed() {
const hugeData = new Array(1000000).fill("x");
const length = hugeData.length; // Extract what you need
document.addEventListener("click", () => {
console.log(length); // Only "length" is closed over, hugeData can be GC'd
});
}Issue 2: Stale Closures in React
function ChatComponent() {
const [message, setMessage] = useState("");
useEffect(() => {
const interval = setInterval(() => {
// ❌ "message" is stale — always the initial value ""
console.log("Current message:", message);
}, 1000);
return () => clearInterval(interval);
}, []); // Empty deps = closure captures initial message
// ✅ Fix: Add message to dependency array
// or use a ref for latest value
}Best Practices
- Use
letinstead ofvarin loops — eliminates the classic closure bug - Be mindful of what you close over — large objects in closures prevent garbage collection
- In React, watch your dependency arrays — stale closures are the #1 hooks bug
- Use closures for encapsulation — it's the JavaScript way to create private state
- Name your closures — for better stack traces in debugging
Interview Tip
When asked about closures, give the definition, then immediately show the loop example. Then explain a practical use case (memoization or module pattern). Bonus points if you mention stale closures in React hooks. That shows real-world experience.