What is a Closure?
A closure is a function that remembers the variables from its outer scope even after that outer function has returned. In other words, a function "closes over" its surrounding environment.
Every function in JavaScript is a closure — but the interesting ones are those that access variables from an outer function after the outer function has finished executing.
MDN Definition: A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).
Lexical Scope — The Foundation
To understand closures you first need to understand lexical scope. Scope in JavaScript is determined by where variables are written in the source code — not where they are called from.
function outer() {
const message = 'Hello from outer!'; // outer scope
function inner() {
console.log(message); // inner can see message
}
inner(); // logs: Hello from outer!
}
outer();The inner function can access message because it is lexically inside the outer function. This "lookup" follows the scope chain — inner → outer → global.
Closure in Action
The real magic of closures happens when the inner function escapes from the outer function — either by being returned or passed as a callback — and continues to remember the outer variables.
function makeCounter() {
let count = 0; // this variable is "closed over"
return function () {
count++;
console.log(count);
};
}
const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3makeCounter has returned, but count is still alive because counter (the returned function) holds a reference to it. Each call increments the same count variable.
Key insight: Closures keep variables alive as long as a function that references them still exists. The variable is not garbage-collected.
Practical Real-World Examples
1. Private Variables (Encapsulation)
Closures let you simulate private state — variables that can't be accessed from the outside.
function createUser(name) {
let loginCount = 0; // private — not accessible outside
return {
login() {
loginCount++;
console.log(`${name} logged in. Total logins: ${loginCount}`);
},
getCount() {
return loginCount;
}
};
}
const user = createUser('Jaydeep');
user.login(); // Jaydeep logged in. Total logins: 1
user.login(); // Jaydeep logged in. Total logins: 2
console.log(user.loginCount); // undefined — truly private!2. Partial Application (Pre-filled Arguments)
function multiply(a) {
return function (b) {
return a * b;
};
}
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 153. Event Handlers with Preserved State
function attachClickLogger(buttonId) {
let clickCount = 0;
document.getElementById(buttonId).addEventListener('click', function () {
clickCount++;
console.log(`Button ${buttonId} clicked ${clickCount} times`);
});
}
attachClickLogger('submitBtn');Closures and Loops — Classic Gotcha
One of the most common interview questions involves closures inside loops.
// WRONG — using var
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // logs 3, 3, 3 — not 0, 1, 2!
}, 1000);
}Why? var is function-scoped (not block-scoped). All three callbacks share the same i variable. By the time they run, the loop has finished and i is 3.
There are two fixes:
// FIX 1 — use let (block-scoped, creates new binding each iteration)
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // 0, 1, 2 — correct!
}, 1000);
}
// FIX 2 — use an IIFE to capture i by value
for (var i = 0; i < 3; i++) {
(function (captured) {
setTimeout(function () {
console.log(captured); // 0, 1, 2 — correct!
}, 1000);
})(i);
}Module Pattern with Closures
Before ES Modules, the module pattern used closures to create a public API while hiding internal state.
const cartModule = (function () {
// private
const items = [];
// public API
return {
addItem(item) {
items.push(item);
},
removeItem(name) {
const idx = items.findIndex(i => i.name === name);
if (idx !== -1) items.splice(idx, 1);
},
getTotal() {
return items.reduce((sum, i) => sum + i.price, 0);
},
getCount() {
return items.length;
}
};
})();
cartModule.addItem({ name: 'Laptop', price: 999 });
cartModule.addItem({ name: 'Mouse', price: 29 });
console.log(cartModule.getTotal()); // 1028
console.log(cartModule.getCount()); // 2IIFE (Immediately Invoked Function Expression) runs once, returns the public object, and the private items array lives on as a closure.
Memoization Using Closures
Memoization is a performance optimization that caches the results of expensive function calls. Closures make it elegant.
function memoize(fn) {
const cache = {}; // closed over by the returned function
return function (...args) {
const key = JSON.stringify(args);
if (cache[key] !== undefined) {
console.log('From cache!');
return cache[key];
}
const result = fn(...args);
cache[key] = result;
return result;
};
}
function slowSquare(n) {
// imagine this is slow
return n * n;
}
const fastSquare = memoize(slowSquare);
fastSquare(5); // computed: 25
fastSquare(5); // From cache! 25
fastSquare(10); // computed: 100Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
Using var in loops with async callbacks |
All callbacks share the same variable | Use let or an IIFE |
| Expecting closures to capture values | Closures capture references, not values | Copy the value into a new variable |
| Creating closures in tight loops | Memory leak — many closures holding large objects | Nullify references when done |
| Mutating shared closed-over state | Unexpected side effects across closures | Use separate closures or immutable data |
Remember: Closures hold references to variables, not copies. If you change the variable after the closure is created, the closure sees the new value.