JavaScript Closures Explained: How They Work With Examples
A closure is a function that remembers the variables around it. Learn how JavaScript closures work, build private state and function factories, fix the classic loop bug and avoid memory traps.
- A closure is a function bundled with the variables from the scope where it was created.
- Those variables stay alive after the outer function has returned, and each call creates a fresh set.
- Closures give you private state, function factories, memoization and working callbacks.
- Use
letorconstin loops. Avarloop gave us[3, 3, 3]instead of[0, 1, 2].
Closures come up in nearly every JavaScript interview and in nearly every real codebase, yet many developers use them for years without being able to explain them. The good news is that JavaScript closures follow one simple rule, and you have probably written several already.
We ran every example below in Node.js, and the comments show the real output. If you need a refresher on functions and variables, start with our JavaScript tutorial for beginners.
Start with lexical scope
To understand closures, you first need lexical scope. It means that a function can see the variables that surround it in the source code, and that this visibility is decided by where the function is written, not by where it is called.
const site = "Stack Quora";
function outer() {
const topic = "closures";
function inner() {
console.log(site, topic);
}
inner();
}
outer(); // Stack Quora closures
inner has no variables of its own, so JavaScript looks outward: first in outer, then in the global scope. This chain of lookups is called the scope chain. Lexical scope is the rule, and a closure is what happens when that rule continues to apply after the outer function has finished.
What a closure is
Normally, local variables disappear when a function returns. But if an inner function still needs them, JavaScript keeps them alive. The inner function together with those remembered variables is a closure.
function makeCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const a = makeCounter();
const b = makeCounter();
console.log(a(), a(), b()); // 1 2 1
Look closely at that output. makeCounter has already returned, yet a still reads and changes count. And b printed 1, not 3, which proves that each call to makeCounter creates a separate count. A closure holds a live link to the variable, not a frozen copy of its value.
Tip: in an interview, say this: “A closure is a function that keeps access to variables from the scope where it was defined, even after that scope has finished.” Then write the counter example.
Private state with closures
Before classes had private fields, closures were the standard way to hide data. They are still a clean option for small modules.
function bank(start) {
let balance = start;
return {
deposit(amount) {
balance += amount;
return balance;
},
get balance() {
return balance;
},
};
}
const account = bank(100);
account.deposit(50);
console.log(account.balance); // 150
console.log(account.bal); // undefined
The only way to change balance is through deposit, so you can add validation there and be sure that no other code can bypass it. Both methods share the same closed-over variable.
| Approach | How data is hidden | Good for |
|---|---|---|
| Closure | Variable lives in the outer function | Small modules, callbacks, hooks |
Class with #private fields |
The language enforces privacy | Many instances, inheritance |
Naming such as _balance |
Convention only, nothing is enforced | Not real privacy |
Function factories
A factory is a function that builds other functions with some settings already fixed. This is also known as partial application.
const multiply = factor => n => n * factor;
const double = multiply(2);
console.log(double(21)); // 42
This pattern is everywhere in practice: a logger with a fixed prefix, an API client with a fixed base URL, or an event handler that knows which item it belongs to. When you add types to code like this, our TypeScript tutorial for beginners shows how to describe function types.
The classic loop bug
This is the most famous closure puzzle. Each function below is meant to return its own loop number.
var fns = [];
for (var i = 0; i < 3; i++) {
fns.push(() => i);
}
console.log(fns.map(f => f())); // [ 3, 3, 3 ]
let fixed = [];
for (let j = 0; j < 3; j++) {
fixed.push(() => j);
}
console.log(fixed.map(f => f())); // [ 0, 1, 2 ]
With var, there is one single i for the whole function. All three closures point to that one variable, which is 3 by the time they run. With let, each loop iteration gets its own new binding, so every closure captures a different one. That is why modern code uses let and const.
Two useful patterns: once and memoize
Once makes sure a function runs a single time, which suits setup work.
function once(fn) {
let done = false, result;
return (...args) => {
if (!done) {
done = true;
result = fn(...args);
}
return result;
};
}
const init = once(() => {
console.log("init runs");
return 42;
});
console.log(init(), init());
// init runs
// 42 42
Memoize remembers earlier results so that repeated calls are cheap.
function memoize(fn) {
const cache = new Map();
return n => {
if (!cache.has(n)) cache.set(n, fn(n));
return cache.get(n);
};
}
let hits = 0;
const square = memoize(n => {
hits++;
return n * n;
});
square(4);
square(4);
square(5);
console.log(hits); // 2
The real function ran twice for three calls, because the second square(4) came from the cache. In both patterns, the remembered state lives in the closure and nowhere else. Python has the same concept, and it powers the wrappers described in Python decorators explained.
Closures in asynchronous code
Whenever you pass a callback to a timer, an event listener or a promise, you are using a closure. The callback runs later, but it can still see the variables from where it was created.
function greetLater(name) {
setTimeout(() => {
console.log("Hello " + name);
}, 1000);
}
greetLater("Asha"); // Hello Asha, after one second
greetLater has long returned when the timer fires, yet name is still available. The same is true inside async functions, which you can read about in our guide to JavaScript async await.
Memory and stale values
Closures are not free, and two problems come up in real projects.
- Retained memory. Anything a live closure can reach stays in memory. A long-lived event listener that closes over a huge array keeps that array alive. Remove listeners when you no longer need them.
- Stale values. A closure sees the variables from the scope in which it was created. In React, a callback from an old render can keep reading old state, which is a very common bug.
- Accidental sharing. Closures created in the same scope share the same variables, so a change made through one is visible through the others. Sometimes this is what you want, and sometimes it is a bug.
- Hard-to-test state. Hidden variables cannot be reset from outside. Offer a reset function, or create a fresh instance for each test.
Do not avoid closures because of this. Engines handle them well, and problems usually appear only when something large is held for a long time. If you suspect a leak, take heap snapshots in your browser’s developer tools and compare them.
A practical closure: debounce
A search box that calls the server on every keystroke wastes requests. Debouncing waits until the user pauses, then runs the function once. It is one of the most common closures you will meet in real frontend code, and it needs only a few lines.
function debounce(fn, wait) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), wait);
};
}
const search = debounce((text) => console.log("Searching:", text), 100);
search("p");
search("pl");
search("pla");
// Searching: pla
We ran this in Node.js, and it printed only the last value. The returned function closes over timer, so every call can see and cancel the timeout set by the previous call. Nothing outside can touch that variable, and each call to debounce creates its own separate timer, so two debounced functions never interfere with each other.
Final thoughts
JavaScript closures are simply functions that remember where they were born. Write the counter from memory, explain the var loop bug out loud and try to build once on your own. After that, you will start to notice closures in every callback you read.
Frequently asked questions
What is a closure in JavaScript?
A closure is a function combined with the variables from the scope in which it was created. It keeps access to them even after the outer function has returned.
Why are closures useful?
They give you private state, function factories, memoization and callbacks that remember their context, all without classes or global variables.
What is the difference between scope and closure?
Scope is the rule that decides which variables code can see. A closure is the result of that rule continuing to work after the outer function has finished.
Do closures cause memory leaks?
They can if a long-lived closure holds on to large objects. Removing event listeners and clearing references lets the garbage collector free the memory.
Why does a var loop print the same number?
All closures share the one var variable, which has its final value by the time they run. With let, each iteration gets its own binding.
Keep learning
Need help with testing, WordPress or SEO?
We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.