What Is a Closure?
A closure is a function that remembers and can access variables from its outer (enclosing) scope, even after the outer function has returned. Every function in JavaScript creates a closure. This isn't a special opt-in feature — it's a fundamental consequence of how the language handles scope and function creation.
function createCounter() {
let count = 0;
return function increment() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
The increment function closes over the count variable. Even after createCounter finishes executing, increment still has access to count. The variable lives on, tucked away in the closure's hidden reference to its creation environment.
Think of it this way: the inner function carries a "backpack" containing all the variables it needs from the outer scope. That backpack goes wherever the function goes, whether it's passed as a callback, stored in a variable, or returned from another function.
┌─────────────────────────────────────┐
│ createCounter() execution │
│ │
│ ┌───────────────┐ │
│ │ count = 0 │ ◄── Environment │
│ └───────┬───────┘ Record │
│ │ │
│ ┌───────▼──────────────────────┐ │
│ │ function increment() { │ │
│ │ count++; // references │ │
│ │ return count; // count │ │
│ │ } │ │
│ │ │ │
│ │ [[Environment]] ─────► count │ │
│ └──────────────────────────────┘ │
│ │
│ return increment; │
└─────────────────────────────────────┘
After createCounter() returns:
┌───────────────────────────────────┐
│ counter (the returned function) │
│ │
│ Still has access to count via │
│ its [[Environment]] reference │
│ │
│ counter() → count becomes 1 │
│ counter() → count becomes 2 │
│ counter() → count becomes 3 │
└───────────────────────────────────┘
How Closures Work Under the Hood
When JavaScript creates a function, it attaches a hidden internal property called [[Environment]] that references the lexical environment where the function was created. This is the engine-level mechanism that makes closures possible.
The Lexical Environment
Every execution context has a lexical environment with two components:
- Environment Record — stores local variables and function declarations
- Outer Reference — points to the parent lexical environment
function outer() {
const x = 10; // stored in outer's Environment Record
function inner() {
console.log(x); // follows Outer Reference to find x
}
return inner;
}
When inner tries to read x, the engine first checks inner's own Environment Record. Not found there, so it follows the Outer Reference to outer's Environment Record where x = 10 lives.
Scope Chain Walkthrough
Let's trace through a more complex example to see the scope chain in action:
const globalVar = 'global';
function level1() {
const l1Var = 'level 1';
function level2() {
const l2Var = 'level 2';
function level3() {
console.log(l2Var); // 'level 2' — found in level2
console.log(l1Var); // 'level 1' — found in level1
console.log(globalVar); // 'global' — found in global
}
return level3;
}
return level2;
}
const fn = level1()();
fn();
Scope chain for level3:
level3 Environment Record
│ (empty — no local vars)
│
▼
level2 Environment Record
│ l2Var = 'level 2'
│
▼
level1 Environment Record
│ l1Var = 'level 1'
│
▼
Global Environment Record
│ globalVar = 'global'
│
▼
null (end of chain)
The key insight: JavaScript determines scope at function creation time (lexical scoping), not at call time. This is why closures always reference the environment where they were defined, not where they're called.
Closures Capture References, Not Values
A critical detail that trips up many developers: closures capture references to variables, not snapshots of their values.
function createFunctions() {
let value = 1;
const getValue = () => value;
const setValue = (v) => { value = v; };
return { getValue, setValue };
}
const { getValue, setValue } = createFunctions();
console.log(getValue()); // 1
setValue(42);
console.log(getValue()); // 42 — same variable, updated reference
Both getValue and setValue close over the same value variable. When setValue changes it, getValue sees the change because they share the same reference.
Common Closure Patterns
Data Privacy (The Module Pattern)
Closures enable private variables — one of JavaScript's most powerful patterns. Before ES6 modules, this was the way to achieve encapsulation.
function createBankAccount(initialBalance) {
let balance = initialBalance;
const transactions = [];
function recordTransaction(type, amount) {
transactions.push({
type,
amount,
balance,
timestamp: Date.now(),
});
}
return {
deposit(amount) {
if (amount <= 0) throw new Error('Deposit must be positive');
balance += amount;
recordTransaction('deposit', amount);
return balance;
},
withdraw(amount) {
if (amount <= 0) throw new Error('Withdrawal must be positive');
if (amount > balance) throw new Error('Insufficient funds');
balance -= amount;
recordTransaction('withdrawal', amount);
return balance;
},
getBalance() {
return balance;
},
getTransactionHistory() {
return [...transactions]; // return copy, not the real array
},
};
}
const account = createBankAccount(100);
account.deposit(50); // 150
account.withdraw(30); // 120
account.getBalance(); // 120
// These are truly private:
// account.balance → undefined
// account.transactions → undefined
// account.recordTransaction → undefined
The Revealing Module Pattern is an extension of this:
const UserModule = (function () {
// private
const users = [];
let nextId = 1;
function validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// public API
function addUser(name, email) {
if (!validateEmail(email)) throw new Error('Invalid email');
const user = { id: nextId++, name, email };
users.push(user);
return user;
}
function getUser(id) {
return users.find((u) => u.id === id) || null;
}
function getUserCount() {
return users.length;
}
return { addUser, getUser, getUserCount };
})();
UserModule.addUser('Alice', '[email protected]');
UserModule.getUserCount(); // 1
// UserModule.users → undefined (private)
// UserModule.validateEmail → undefined (private)
Function Factories
Create specialized functions from a general template. Each returned function closes over its own copy of the parameters.
function multiply(factor) {
return function (number) {
return number * factor;
};
}
const double = multiply(2);
const triple = multiply(3);
const toPercent = multiply(100);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(toPercent(0.5)); // 50
A more practical factory — creating validators:
function createValidator(rules) {
return function validate(value) {
const errors = [];
for (const rule of rules) {
if (!rule.test(value)) {
errors.push(rule.message);
}
}
return { valid: errors.length === 0, errors };
};
}
const validatePassword = createValidator([
{ test: (v) => v.length >= 8, message: 'Must be at least 8 characters' },
{ test: (v) => /[A-Z]/.test(v), message: 'Must contain uppercase letter' },
{ test: (v) => /[0-9]/.test(v), message: 'Must contain a number' },
{ test: (v) => /[^A-Za-z0-9]/.test(v), message: 'Must contain special char' },
]);
validatePassword('hello');
// { valid: false, errors: ['Must be at least 8 characters', ...] }
validatePassword('Str0ng!Pass');
// { valid: true, errors: [] }
Memoization
Cache expensive computation results using closures. The cache lives in the closure's scope — invisible to the outside world.
function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) 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); // 25 (cached, no log)
Here's a more robust memoization with TTL (time-to-live):
function memoizeWithTTL(fn, ttlMs = 60_000) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
const { value, expiresAt } = cache.get(key);
if (Date.now() < expiresAt) return value;
cache.delete(key); // expired
}
const result = fn.apply(this, args);
cache.set(key, { value: result, expiresAt: Date.now() + ttlMs });
return result;
};
}
const fetchUserData = memoizeWithTTL(async (userId) => {
const res = await fetch(`/api/users/${userId}`);
return res.json();
}, 30_000); // cache for 30 seconds
Partial Application and Currying
Closures are the backbone of partial application — pre-filling some arguments of a function.
function partial(fn, ...presetArgs) {
return function (...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
function log(level, category, message) {
console.log(`[${level}] [${category}] ${message}`);
}
const logError = partial(log, 'ERROR');
const logErrorDB = partial(log, 'ERROR', 'DATABASE');
logError('AUTH', 'Login failed'); // [ERROR] [AUTH] Login failed
logErrorDB('Connection timeout'); // [ERROR] [DATABASE] Connection timeout
Full currying with closures:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function (...moreArgs) {
return curried.apply(this, [...args, ...moreArgs]);
};
};
}
const add = curry((a, b, c) => a + b + c);
add(1)(2)(3); // 6
add(1, 2)(3); // 6
add(1)(2, 3); // 6
add(1, 2, 3); // 6
The Classic Loop Trap
One of the most common closure mistakes, and the most frequently asked about in interviews:
// Bug: all callbacks log 3
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3
Why does this happen? Because var is function-scoped, not block-scoped. There's only one i variable shared across all three iterations. By the time the setTimeout callbacks run, the loop is done and i is 3.
Timeline:
Loop runs: i=0 → i=1 → i=2 → i=3 (loop ends)
|
Callbacks: ├── setTimeout callback 1: log(i) → 3
├── setTimeout callback 2: log(i) → 3
└── setTimeout callback 3: log(i) → 3
All three callbacks reference the SAME i variable.
Fix 1: Use let (block scoping)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2
let creates a new binding for each iteration, so each callback closes over its own copy of i.
Fix 2: IIFE (Immediately Invoked Function Expression)
The pre-ES6 solution — create a new scope manually:
for (var i = 0; i < 3; i++) {
(function (j) {
setTimeout(() => console.log(j), 100);
})(i);
}
// Output: 0, 1, 2
Fix 3: forEach (avoids the problem entirely)
[0, 1, 2].forEach((i) => {
setTimeout(() => console.log(i), 100);
});
// Output: 0, 1, 2
Each iteration of forEach gets its own function scope with its own i parameter.
Closures in Event Listeners
Event handlers are one of the most common real-world uses of closures. The handler function closes over variables from its enclosing scope.
function setupButtonTracking(buttonId) {
let clickCount = 0;
const button = document.getElementById(buttonId);
button.addEventListener('click', function handleClick() {
clickCount++;
console.log(`Button ${buttonId} clicked ${clickCount} times`);
if (clickCount >= 10) {
button.removeEventListener('click', handleClick);
console.log('Tracking complete — listener removed');
}
});
}
setupButtonTracking('submit-btn');
The handleClick function closes over clickCount, buttonId, and button. Each call to setupButtonTracking creates a separate closure with its own clickCount.
A practical pattern — debouncing with closures:
function debounce(fn, delayMs) {
let timerId = null;
return function (...args) {
clearTimeout(timerId);
timerId = setTimeout(() => {
fn.apply(this, args);
}, delayMs);
};
}
const searchInput = document.getElementById('search');
searchInput.addEventListener(
'input',
debounce((event) => {
console.log('Searching for:', event.target.value);
// fetch search results...
}, 300)
);
The returned debounce handler closes over timerId. Each call to debounce creates its own timer reference.
And throttling:
function throttle(fn, limitMs) {
let inThrottle = false;
return function (...args) {
if (inThrottle) return;
fn.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limitMs);
};
}
window.addEventListener(
'scroll',
throttle(() => {
console.log('Scroll position:', window.scrollY);
}, 200)
);
Closures in React
React hooks rely heavily on closures. Understanding closures is essential to understanding hooks.
useState — Closure Under the Hood
// Simplified version of how useState works internally
function createState(initialValue) {
let state = initialValue;
function getState() {
return state;
}
function setState(newValue) {
state = typeof newValue === 'function' ? newValue(state) : newValue;
// trigger re-render...
}
return [getState, setState];
}
The Stale Closure Problem in React
One of the most common React bugs stems from closures capturing old values:
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
// BUG: this closes over the initial count (0)
// It always sets count to 0 + 1 = 1
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []); // empty deps — effect never re-runs
return <div>{count}</div>; // stuck at 1
}
The fix — use the functional updater form:
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
// FIX: functional update doesn't depend on closure value
setCount((prev) => prev + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <div>{count}</div>; // works correctly
}
Custom Hooks Use Closures
Every custom hook is a closure pattern:
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
// setValue closes over key and setStoredValue
const setValue = (value) => {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue];
}
// Usage
const [theme, setTheme] = useLocalStorage('theme', 'dark');
useCallback and Closures
useCallback exists precisely because of closure behavior:
function SearchResults({ query }) {
const [results, setResults] = useState([]);
// Without useCallback, this creates a new function (closure)
// on every render, which might cause child re-renders
const handleSelect = useCallback(
(item) => {
console.log(`Selected ${item.name} for query: ${query}`);
},
[query] // re-create only when query changes
);
return (
<ul>
{results.map((item) => (
<ResultItem key={item.id} item={item} onSelect={handleSelect} />
))}
</ul>
);
}
Closures for Iterators and Generators
Closures power custom iterators:
function createRangeIterator(start, end, step = 1) {
let current = start;
return {
next() {
if (current <= end) {
const value = current;
current += step;
return { value, done: false };
}
return { value: undefined, done: true };
},
[Symbol.iterator]() {
return this;
},
};
}
const range = createRangeIterator(1, 5);
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
Infinite sequence generator using closures:
function fibonacci() {
let a = 0;
let b = 1;
return function next() {
const value = a;
[a, b] = [b, a + b];
return value;
};
}
const fib = fibonacci();
console.log(fib()); // 0
console.log(fib()); // 1
console.log(fib()); // 1
console.log(fib()); // 2
console.log(fib()); // 3
console.log(fib()); // 5
Advanced: Closure Composition
Composing closures for pipeline-style operations:
function pipe(...fns) {
return function (initialValue) {
return fns.reduce((acc, fn) => fn(acc), initialValue);
};
}
const processUser = pipe(
(user) => ({ ...user, name: user.name.trim() }),
(user) => ({ ...user, email: user.email.toLowerCase() }),
(user) => ({ ...user, createdAt: Date.now() }),
(user) => Object.freeze(user)
);
const user = processUser({ name: ' Alice ', email: '[email protected]' });
// { name: 'Alice', email: '[email protected]', createdAt: 1709312400000 }
Memory Considerations and Performance
Closures keep references to outer variables alive, preventing garbage collection. This is usually fine, but can cause issues at scale.
The Memory Leak Pattern
function createLargeDataProcessor() {
const largeData = new Array(1_000_000).fill('data');
return {
process() {
return largeData.length;
},
};
}
// largeData is kept in memory as long as processor exists
const processor = createLargeDataProcessor();
Fix: Copy Only What You Need
function createLargeDataProcessor() {
const largeData = new Array(1_000_000).fill('data');
const length = largeData.length; // extract what we need
// largeData is now eligible for GC
return {
process() {
return length;
},
};
}
Accidental Closure Retention
A subtler case — unused closures keeping variables alive:
function setup() {
const hugeArray = new Array(10_000_000).fill(0);
// This function uses hugeArray
function processData() {
return hugeArray.reduce((a, b) => a + b, 0);
}
// This function does NOT use hugeArray, but since it shares
// the same lexical environment, some engines may keep
// hugeArray alive anyway
function getTimestamp() {
return Date.now();
}
return { processData, getTimestamp };
}
Most modern engines (V8, SpiderMonkey) are smart enough to only retain referenced variables, but it's worth being aware of.
Performance Comparison
┌──────────────────────────┬────────────┬──────────────────────┐
│ Pattern │ Memory │ When to Use │
├──────────────────────────┼────────────┼──────────────────────┤
│ Simple closure │ Minimal │ Always fine │
│ Closure over large data │ High │ Be careful │
│ Memoize cache (unbounded)│ Grows │ Add size limit or TTL │
│ Event listener closures │ Moderate │ Remove on cleanup │
│ Closure in loop (x1000) │ Moderate │ Consider alternatives │
└──────────────────────────┴────────────┴──────────────────────┘
Common Interview Questions
Question 1: What will this output?
function createFns() {
const fns = [];
for (var i = 0; i < 5; i++) {
fns.push(function () {
return i;
});
}
return fns;
}
const fns = createFns();
console.log(fns[0]()); // ?
console.log(fns[2]()); // ?
console.log(fns[4]()); // ?
Answer: All three log 5. Classic loop closure trap with var. All functions share the same i, which is 5 after the loop ends.
Question 2: Implement a once function
function once(fn) {
let called = false;
let result;
return function (...args) {
if (called) return result;
called = true;
result = fn.apply(this, args);
return result;
};
}
const initializeDB = once(() => {
console.log('DB initialized');
return { connected: true };
});
initializeDB(); // 'DB initialized' → { connected: true }
initializeDB(); // no log → { connected: true } (cached)
initializeDB(); // no log → { connected: true } (cached)
Question 3: What's wrong with this code?
for (var i = 0; i < 5; i++) {
document.getElementById(`btn-${i}`).addEventListener('click', function () {
alert(`Button ${i} clicked`);
});
}
Answer: All buttons will alert "Button 5 clicked" because the closure captures the reference to i, and i is 5 after the loop. Fix by using let instead of var, or wrapping in an IIFE.
Question 4: Implement createMultiplier with method chaining
function createMultiplier(initial) {
let value = initial;
const api = {
times(n) {
value *= n;
return api; // enables chaining via closure
},
plus(n) {
value += n;
return api;
},
minus(n) {
value -= n;
return api;
},
result() {
return value;
},
};
return api;
}
createMultiplier(2).times(3).plus(4).times(2).result(); // (2*3 + 4) * 2 = 20
Question 5: Explain closure vs scope
| Concept | Definition | Lifetime |
|---|---|---|
| Scope | Determines variable accessibility at a given location in code | Exists during execution of that block/function |
| Closure | A function + its reference to outer scope variables | Persists as long as the function reference exists |
Scope defines the rules. Closures are a consequence of those rules applied to first-class functions.
Question 6: Private counter with reset
function createCounter(start = 0) {
let count = start;
const initial = start;
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count,
reset: () => {
count = initial;
return count;
},
};
}
const c = createCounter(10);
c.increment(); // 11
c.increment(); // 12
c.decrement(); // 11
c.reset(); // 10
Closures vs Other Patterns
┌──────────────────┬─────────────────────┬──────────────────────────┐
│ Pattern │ Privacy │ Use Case │
├──────────────────┼─────────────────────┼──────────────────────────┤
│ Closure │ True private vars │ Factories, modules, │
│ │ │ encapsulation │
├──────────────────┼─────────────────────┼──────────────────────────┤
│ ES6 Class + │ # prefix for │ OOP, when you need │
│ Private Fields │ private fields │ inheritance │
├──────────────────┼─────────────────────┼──────────────────────────┤
│ WeakMap │ External private │ When you need private │
│ │ store │ data on class instances │
├──────────────────┼─────────────────────┼──────────────────────────┤
│ Symbol │ Obscured (not │ Semi-private properties │
│ │ truly private) │ │
├──────────────────┼─────────────────────┼──────────────────────────┤
│ ES Modules │ Module-scoped │ File-level encapsulation │
│ │ (not exported) │ │
└──────────────────┴─────────────────────┴──────────────────────────┘
Debugging Closures
Chrome DevTools shows closure variables in the Scope panel:
function outer() {
const secret = 42;
return function inner() {
debugger; // open DevTools, check Scope panel
return secret;
};
}
const fn = outer();
fn();
In the Scope panel, you'll see:
- Local —
inner's own variables - Closure (outer) — variables closed over from
outer(secret: 42) - Global — global scope
You can also inspect closures programmatically (non-standard, V8 only):
// Chrome DevTools console only
console.dir(fn);
// Expand [[Scopes]] to see closure variables
Real-World Pattern: Request Manager
A production-grade example combining multiple closure patterns:
function createRequestManager(baseURL, defaultHeaders = {}) {
let requestCount = 0;
const pendingRequests = new Map();
const interceptors = { request: [], response: [] };
async function request(method, path, options = {}) {
const url = `${baseURL}${path}`;
const requestId = ++requestCount;
const headers = {
...defaultHeaders,
...options.headers,
'X-Request-ID': requestId,
};
// Run request interceptors
let config = { method, url, headers, body: options.body };
for (const interceptor of interceptors.request) {
config = await interceptor(config);
}
const controller = new AbortController();
pendingRequests.set(requestId, controller);
try {
const response = await fetch(config.url, {
method: config.method,
headers: config.headers,
body: config.body ? JSON.stringify(config.body) : undefined,
signal: controller.signal,
});
let data = await response.json();
// Run response interceptors
for (const interceptor of interceptors.response) {
data = await interceptor(data, response);
}
return data;
} finally {
pendingRequests.delete(requestId);
}
}
return {
get: (path, opts) => request('GET', path, opts),
post: (path, opts) => request('POST', path, opts),
put: (path, opts) => request('PUT', path, opts),
delete: (path, opts) => request('DELETE', path, opts),
addRequestInterceptor(fn) {
interceptors.request.push(fn);
},
addResponseInterceptor(fn) {
interceptors.response.push(fn);
},
cancelAll() {
for (const [id, controller] of pendingRequests) {
controller.abort();
pendingRequests.delete(id);
}
},
getPendingCount: () => pendingRequests.size,
getTotalRequests: () => requestCount,
};
}
const api = createRequestManager('https://api.example.com', {
'Content-Type': 'application/json',
});
api.addRequestInterceptor((config) => {
config.headers['Authorization'] = `Bearer ${getToken()}`;
return config;
});
const users = await api.get('/users');
Everything private — requestCount, pendingRequests, interceptors, the request function itself — hidden behind closures.
Key Takeaways
- Every function is a closure — it captures a reference to its lexical environment
- Closures capture references, not values — if the outer variable changes, the closure sees the change
- Use closures for privacy — they provide true encapsulation without classes
- Watch for the loop trap — use
letor IIFE to create per-iteration scopes - Be mindful of memory — closures keep outer variables alive; free large data when possible
- Stale closures in React — use functional updates or add dependencies to avoid capturing old values
- Closures power most JavaScript patterns — callbacks, promises, iterators, hooks, module pattern
- Debug in DevTools — the Scope panel shows closure variables clearly
- No performance penalty — closures are a fundamental language feature, not an optimization concern
- Prefer closures over global state — they keep related state together and isolated