Closures in JavaScript: A Complete Guide
A complete guide to JavaScript closures explaining lexical scope, the scope chain, and how closures power data privacy, memoization, and function factories.
Introduction
If you have written more than a few functions in JavaScript, you have almost certainly used a closure without realizing it. Closures are one of those concepts that sound intimidating in interviews but turn out to be something you already understand intuitively once you see them explained with real code. In simple terms, a closure is what happens when a function "remembers" the variables from the place where it was created, even after that place has finished executing.
Understanding closures properly will change how you read JavaScript code. Callbacks, event handlers, module patterns, memoized functions, and even React hooks all lean on closures under the hood. This guide walks through what closures actually are, why they exist, and how to use them deliberately instead of accidentally.
What Exactly Is a Closure?
Every function in JavaScript carries a hidden reference to the environment in which it was defined. This environment includes all the variables that were in scope at that point, not just the ones the function directly uses. When you return a function from another function, or pass a function around as a callback, it drags that environment along with it. That combination — a function plus its remembered environment — is what we call a closure.
function makeCounter() {
let count = 0;
return function increment() {
count += 1;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
Notice that count is a local variable inside makeCounter. Normally, local variables disappear once a function returns. But here, the inner increment function keeps a live reference to count, so it survives. Each call to makeCounter() creates a brand new closure with its own independent count, which is why closures are so useful for creating isolated, stateful pieces of behavior.
Lexical Scope and the Scope Chain
Closures exist because JavaScript uses lexical scoping, meaning a function's access to variables is determined by where it is written in the source code, not by how or where it is called. When the JavaScript engine looks up a variable inside a function, it first checks the function's own local scope, then walks outward through each enclosing scope until it finds a match or reaches the global scope. This chain of enclosing scopes is called the scope chain.
A closure is essentially a snapshot of that scope chain, kept alive for as long as the inner function might still be called. This is different from many beginners' mental model of "variables get deleted when the function ends." In reality, a variable is only garbage collected once nothing can reference it anymore — and a closure is a reference.
Closures in Action: Three Practical Examples
Data Privacy with the Module Pattern
Before ES modules and private class fields existed, developers used closures to fake private variables:
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) throw new Error("Insufficient funds");
balance -= amount;
return balance;
},
getBalance() {
return balance;
},
};
}
const account = createBankAccount(100);
account.deposit(50);
console.log(account.getBalance()); // 150
There is no way to reach balance directly from outside — the only access points are the methods that the closure exposes. This pattern is still widely used for building small, self-contained utilities.
Function Factories
Closures let you generate specialized functions from a general template, which keeps code DRY:
function multiplyBy(factor) {
return (n) => n * factor;
}
const double = multiplyBy(2);
const triple = multiplyBy(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
Memoization with Closures
Closures also power memoization, a caching technique that avoids repeating expensive calculations:
function memoize(fn) {
const cache = new Map();
return (arg) => {
if (cache.has(arg)) return cache.get(arg);
const result = fn(arg);
cache.set(arg, result);
return result;
};
}
const slowSquare = (n) => {
for (let i = 0; i < 1e6; i++) {} // simulate work
return n * n;
};
const fastSquare = memoize(slowSquare);
The cache variable lives inside the closure created by memoize, invisible to the outside world but persistent across every call to the returned function.
The Classic Loop Problem
A well-known closure pitfall involves loops and var:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Logs: 3, 3, 3
Because var is function-scoped, all three callbacks close over the same i, and by the time the timeouts fire, the loop has already finished with i equal to 3. Switching to let solves this because let creates a new binding for every iteration:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Logs: 0, 1, 2
Best Practices for Working with Closures
- Prefer
letandconstovervarso loop variables behave predictably inside closures. - Use closures deliberately for encapsulation instead of exposing every internal variable on an object.
- Keep closures small; capturing an entire large object when you only need one property makes debugging harder and can retain more memory than necessary.
- Name your inner functions when it helps readability, especially in stack traces during debugging.
- Combine closures with higher-order functions like
map,filter, and custom factories to write reusable, composable code.
Common Mistakes to Avoid
- Assuming a closure "copies" a variable's value at creation time — it actually keeps a live reference, so later mutations are visible.
- Creating closures inside tight loops that capture large objects, which can quietly bloat memory usage in long-running applications.
- Forgetting that class methods and arrow functions used as event handlers also form closures over
thisand outer variables, which can lead to stale state bugs if not handled carefully. - Overusing closures for things that a simple parameter or return value would express more clearly.
Closures and Memory: What Actually Gets Retained
A subtlety worth understanding is exactly what a closure keeps alive. It is tempting to assume a closure captures "the whole outer function," but in practice JavaScript engines are smart enough to retain only the specific variables the inner function actually references, not everything that happened to be in scope.
function outer() {
const bigData = new Array(1_000_000).fill("x"); // large, unused by the inner function
const small = 42;
return function inner() {
return small; // only references `small`
};
}
Modern JavaScript engines can often garbage-collect bigData here because inner never touches it, even though both variables technically live in the same enclosing scope. This is not guaranteed by the language specification, but it is a common optimization, and it means closures are usually cheaper than a naive mental model would suggest. That said, when a closure does reference a large object — say, caching a big response payload — that object stays alive for as long as the closure itself is reachable, which is worth keeping in mind for long-lived closures like event handlers or module-level caches that never go out of scope.
Conclusion
Closures are not a special syntax you opt into — they are a natural consequence of how JavaScript scopes and executes functions. Once you can spot them, you will notice closures everywhere: in event listeners, in React's useState, in Node.js callbacks, and in nearly every utility library you use. Practice by rewriting a few of your own functions with the module pattern or memoization technique above, and closures will quickly move from "confusing interview topic" to "just how JavaScript works."
Article FAQ
References
Comments
Comments are coming soon. Meanwhile, share your feedback via our contact page.
Related Articles
View allThe JavaScript Event Loop Explained
Understand how the JavaScript event loop works, including the call stack, task queue, and microtask queue, with clear diagrams-in-text and runnable examples.
JavaScript Array Methods Deep Dive
A practical deep dive into the JavaScript array methods you use every day, including map, filter, reduce, find, and sort, with real examples and performance...
Async/Await in JavaScript Explained
Learn how async/await works under the hood in JavaScript, how it relates to Promises, and how to handle errors, parallelism, and common pitfalls correctly.
Top JavaScript Interview Questions and Answers
A curated list of common JavaScript interview questions with clear explanations, covering closures, hoisting, the event loop, prototypes, and equality compar...