Explain closures in JavaScript
The Short Answer
A closure is a function that remembers the variables from its outer scope even after that outer function has finished executing. Every function in JavaScript forms a closure — it captures a reference to the variables in its surrounding lexical environment. This is what lets inner functions access outer variables long after the outer function has returned.
How Closures Form
When a function is defined inside another function, the inner function gets a permanent link to the outer function's variable environment. Even when the outer function finishes and its execution context is popped off the call stack, the variables it created stay alive in memory because the inner function still references them.
function createCounter() {
let count = 0; // This variable lives on after createCounter returns
return function increment() {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count is not accessible from outside
// console.log(count); // ReferenceError: count is not defined
// Each call to createCounter creates a NEW closure with its own count
const counter2 = createCounter();
console.log(counter2()); // 1 (independent from counter)
The key insight: count isn't copied into the inner function — the inner function holds a reference to the same variable. That's why calling counter() multiple times increments the same count. And each call to createCounter() creates a completely independent closure with its own count.
Lexical Scoping — The Foundation
Closures work because JavaScript uses lexical scoping — a function's scope is determined by where it's written in the source code, not where it's called. When the engine encounters a variable, it walks up the scope chain from the current function outward until it finds a match. Closures are just this scope chain being preserved.
const globalVar = 'global';
function outer() {
const outerVar = 'outer';
function middle() {
const middleVar = 'middle';
function inner() {
// inner can access all variables in its lexical scope chain
console.log(globalVar); // 'global'
console.log(outerVar); // 'outer'
console.log(middleVar); // 'middle'
}
return inner;
}
return middle;
}
const middleFn = outer(); // outer() finishes, but outerVar lives on
const innerFn = middleFn(); // middle() finishes, but middleVar lives on
innerFn(); // Still has access to all three variables
Even though outer() and middle() have both returned, their local variables persist because innerFn still references them through the scope chain. The garbage collector can't clean them up while a closure holds a reference.
Practical Use Cases
Data Privacy / Encapsulation
Closures let you create truly private variables in JavaScript — variables that can only be accessed and modified through specific functions you expose. This is the module pattern's foundation and how you achieve encapsulation without classes.
function createBankAccount(initialBalance: number) {
let balance = initialBalance; // Private — no direct access
return {
deposit(amount: number) {
if (amount <= 0) throw new Error('Amount must be positive');
balance += amount;
return balance;
},
withdraw(amount: number) {
if (amount > balance) throw new Error('Insufficient funds');
balance -= amount;
return balance;
},
getBalance() {
return balance;
},
};
}
const account = createBankAccount(100);
account.deposit(50); // 150
account.withdraw(30); // 120
account.getBalance(); // 120
// account.balance → undefined (truly private)
Function Factories
Closures let you create specialized functions from a general template. The outer function takes configuration, and the returned function uses that configuration every time it's called — without needing to pass it again.
function createMultiplier(factor: number) {
return (value: number) => value * factor;
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const toPercent = createMultiplier(100);
double(5); // 10
triple(5); // 15
toPercent(0.75); // 75
Event Handlers and Callbacks
Every time you use a callback that references a variable from its surrounding scope, you're using a closure. This is extremely common in event handlers, timers, and async operations where the callback runs later but still needs access to variables from when it was created.
function setupButton(buttonId: string, message: string) {
const button = document.getElementById(buttonId);
// This callback closes over 'message'
button?.addEventListener('click', () => {
alert(message); // message is remembered even though setupButton has returned
});
}
setupButton('btn-hello', 'Hello!');
setupButton('btn-bye', 'Goodbye!');
// Each button has its own closure with its own message
The Classic Loop Trap
The most infamous closure gotcha involves loops with var. Because var is function-scoped (not block-scoped), all iterations share the same variable. By the time the callbacks run, the loop has finished and the variable holds its final value. This trips up developers who expect each callback to capture its own iteration value.
// ❌ The trap — all callbacks see i = 3
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 3, 3, 3 (not 0, 1, 2)
}, 100);
}
// ✅ Fix 1: Use let (block-scoped — each iteration gets its own i)
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 0, 1, 2
}, 100);
}
// ✅ Fix 2: IIFE creates a new scope per iteration
for (var i = 0; i < 3; i++) {
((captured) => {
setTimeout(() => {
console.log(captured); // 0, 1, 2
}, 100);
})(i);
}
With let, each loop iteration creates a new block scope with its own copy of i. The closure in each setTimeout captures a different variable. With var, there's only one i shared across all iterations — every closure points to the same variable.
Memory Implications
Closures keep their referenced variables alive in memory. This is usually fine, but can cause memory leaks if you're not careful — especially with event listeners or long-lived callbacks that close over large objects you no longer need.
Avoiding closure memory leaks
- ✅Remove event listeners when components unmount
- ✅Clear intervals and timeouts when no longer needed
- ✅Set closed-over references to null when done with them
- ✅Be mindful of closures in long-lived caches or singleton patterns
Why Interviewers Ask This
Closures are fundamental to JavaScript — they power modules, callbacks, event handlers, React hooks, and most design patterns. Interviewers ask this to check if you understand lexical scoping, can explain why the loop trap happens, know practical applications (data privacy, factories, partial application), and understand the memory implications. A strong answer demonstrates both theoretical understanding and practical experience.
Quick Revision Cheat Sheet
Definition: A function that retains access to its outer scope's variables after the outer function returns
Mechanism: Lexical scoping — scope determined by source code position, not call site
Key insight: Closures capture references to variables, not copies of values
Loop trap: var is function-scoped — use let for per-iteration closures
Use cases: Data privacy, function factories, callbacks, partial application
Memory: Closed-over variables can't be GC'd while the closure exists