JavaScript Closures
Learn how closures let functions remember variables from their outer scope with clear examples.
TL;DR
- 01Inner functions remember variables from their parent scope.
- 02Use closures to keep data private and persistent.
- 03Lexical scope means variables come from where functions are defined.
Tips
- 01Use the module pattern with closures to expose only the functions you want, keeping internal state hidden from outside code.
- 02Reach for closures over classes when you need a small amount of private state and just a couple of methods.
Warnings
- 01Closures keep their captured variables in memory, so avoid creating many closures over large objects to prevent memory leaks.
- 02Using var instead of let inside a loop makes every closure share one variable, a classic source of off-by-one bugs.
What Closures Are
ClosureA function that keeps access to its outer scope after that scope returns.
function outer() {
let n = 0;
return () => ++n;
}
const count = outer();
count(); // 1NestingClosures form automatically whenever you nest one function inside another.
MemoryOuter variables stay alive as long as the inner function referencing them exists.
Lexical scopeVariables resolve based on where a function is defined, not where it's called from.
Every functionEvery JavaScript function creates a closure over its surrounding scope.
Closures and Loops
var in a loopAll closures inside a var-based loop share the same variable reference.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i));
}
// logs 3, 3, 3let in a looplet creates a fresh binding on every iteration.
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i));
}
// logs 0, 1, 2IIFE workaroundWrap the loop body in an IIFE to capture the current value with var.
for (var i = 0; i < 3; i++) {
(() => console.log(i))();
}
// logs 0, 1, 2Common bugThis is one of the most common closure bugs — prefer let in loops by default.
forEachThe same var-sharing issue shows up in forEach callbacks that rely on var.
Private Variables
Hide stateClosures let you hide internal state from the rest of your code.
function createCounter() {
let count = 0;
return { increment: () => ++count, get: () => count };
}
const c = createCounter();
c.increment(); // 1Expose methodsReturn an object with methods that read and update the private values.
Controlled accessThe outside world can only change state through the methods you provide.
Common usesThis pattern suits counters, configuration, and small data stores.
SafetyPrivate variables prevent accidental changes from unrelated code.
Function Factories
Share behaviorCreate functions that share behavior but each remember their own values.
function memoize(fn) {
const cache = new Map();
return x => cache.has(x) ? cache.get(x) : cache.set(x, fn(x)).get(x);
}
const square = memoize(x => x * x);
square(5); // 25, cached afterPrivate cacheThe closure keeps cache alive and private, with no global variable needed.
Real-world useThis pattern powers React's useMemo hook and many utility libraries.
Any arityAdd a rest parameter to memoize functions that take multiple arguments.
Pure functions onlyReserve memoization for pure functions with consistent, side-effect-free outputs.
In Practice
A debounce factory closes over a timer id so repeated calls only trigger the wrapped function once, after the user stops typing.
- 01
debouncereturns a new function that closes over a singletimervariable shared across every call. - 02Each invocation clears any pending timer before scheduling a new one, cancelling the previous call.
- 03The closure keeps
timeralive between calls without exposing it as a global variable. - 04
...argsforwards whatever arguments the event handler passes through to the original function.
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
const search = query => console.log('Searching for:', query);
const debouncedSearch = debounce(search, 300);
input.addEventListener('input', e => debouncedSearch(e.target.value));
// only the last keystroke within 300ms triggers a searchFAQ
All closures created inside a var-based loop share a reference to the same variable. By the time they execute, that variable holds the final loop value. Switch to let in the loop declaration to get a fresh binding per iteration. Alternatively, wrap the closure in an IIFE to capture the current value.
Declare variables inside an outer function and return only the inner functions that need to access them. Those inner functions close over the private variables, which stay inaccessible to external code. This is the foundation of the module pattern, common for encapsulating state without classes.
Every function is technically a closure, but the term becomes meaningful in a specific case. That case is when an inner function references variables from a scope that has already returned. The inner function then keeps those variables alive beyond the outer function's lifetime. A function that only uses its own parameters or globals isn't considered a closure in practice.
A factory function accepts configuration as arguments and returns a new function that closes over those arguments. Each call to the factory then produces a specialized version. For example, makeAdder(5) returns a function that always adds 5 to its input. This avoids repetitive function definitions and keeps logic parameterized.
A closure prevents garbage collection of anything it references. A closure that accidentally captures a large DOM element or dataset keeps it in memory the whole time it lives. Avoid capturing objects you don't need — destructure only the specific values required. You can also set large references to null once the closure no longer needs them.