By WebNest Studio Engineering Team
JavaScript Tutorial
JavaScript Closures
A closure is created when a function "remembers" the variables from the scope in which it was defined, even after that outer scope has finished executing. Closures are not a special syntax you opt into — they happen automatically any time a function is defined inside another function and refers to that outer function's variables.
Closures are one of the most powerful and most misunderstood features of JavaScript. They enable patterns like data privacy, function factories, and memoization, and they explain why some variables in JavaScript seem to "stay alive" long after the function that created them has returned.
Why it matters
A closure is a function that remembers the variables around it after the outer function has finished. Every event handler, timer callback and React hook relies on this. It is also the single most asked concept in JavaScript interviews, so being able to explain it with a small example is worth the practice.
How a Closure Forms
When a function is defined, it keeps a reference to the scope it was created in — not just the values that existed at that moment, but a live link to the variables themselves. If that function is returned or passed elsewhere and called later, it still has access to those outer variables, and can even modify them, because the reference persists as long as the inner function exists.
Why Closures Matter: Data Privacy
Closures let you create private state that cannot be accessed directly from outside a function, only through functions you deliberately expose. This mimics private fields long before JavaScript classes gained a native private-field syntax, and it remains a common and idiomatic pattern.
A Concrete Example: The Counter Pattern
The classic closure example is a counter: an outer function declares a count variable and returns an inner function that increments and returns it. Each call to the outer function creates a brand-new, independent count variable and a brand-new closure over it, so multiple counters never interfere with each other.
Examples
A counter built entirely with a closure over a private variable
function createCounter() {
let count = 0; // private to this closure
return function increment() {
count += 1;
return count;
};
}
const counterA = createCounter();
const counterB = createCounter();
console.log(counterA()); // 1
console.log(counterA()); // 2
console.log(counterA()); // 3
console.log(counterB()); // 1 — independent closure, unaffected by counterA
1
2
3
1
Closures capturing a variable per loop iteration using let
function makeGreeters() {
const greeters = [];
for (let i = 0; i < 3; i++) {
greeters.push(() => `Greeter #${i}`);
}
return greeters;
}
const greeters = makeGreeters();
console.log(greeters[0]());
console.log(greeters[1]());
console.log(greeters[2]());
Greeter #0
Greeter #1
Greeter #2
Common Mistakes
- Using "var" instead of "let" in a loop that creates closures, causing every closure to share the same final value of the loop variable instead of its own per-iteration value.
- Believing a closure captures a snapshot (copy) of a variable's value — it actually keeps a live reference, so later changes to the variable are visible inside the closure.
- Creating closures unnecessarily inside hot loops or frequently called functions, which can hold onto memory longer than needed if the outer variables are large.
- Assuming closures require special syntax — any nested function that references an outer variable already forms a closure automatically.
Exercise
Write makeCounter, which returns a function. Each call of the returned function increases a private count and returns it. Two counters made by separate calls must not affect each other.
Expected output: 1, 2, 1 (one per line)
function makeCounter() {
// TODO: a private variable, and a returned function that increases and returns it
}
const first = makeCounter();
console.log(first());
console.log(first());
const second = makeCounter();
console.log(second());
Show hints
- Declare
let count = 0insidemakeCounter, outside the returned function. - The returned function can still read and change
countaftermakeCounterhas returned.
Show solution
function makeCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const first = makeCounter();
console.log(first());
console.log(first());
const second = makeCounter();
console.log(second());
Quick quiz
1. What is a closure?
- A. A function with no parameters
- B. A way to end a program
- C. A function that has finished running
- D. A function together with the variables of the scope in which it was created
Show answer
D. The function keeps access to those variables for as long as it exists.
2. What do three timers created in for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0) print?
- A. 3, 3, 3
- B. 0, 1, 2
- C. 0, 0, 0
- D. 2, 2, 2
Show answer
A. There is one shared i, and it is 3 by the time the callbacks run. With let, each iteration gets its own i and the output is 0, 1, 2.
3. What is a common practical use of closures?
- A. Making code run faster
- B. Keeping data private to a function, such as a counter or a cache
- C. Avoiding the use of functions
- D. Declaring global variables
Show answer
B. The enclosed variables cannot be reached from outside except through the returned function.
Interview questions
Explain closures with an example.
Show answer
A closure is formed when a function is defined inside another and keeps access to the outer function's variables after the outer function has returned. In a counter, makeCounter declares count and returns a function that increments it. The returned function still sees count, and nothing else can reach it, so the value is private. Each call to makeCounter creates a separate count.
Can closures cause memory problems?
Show answer
Yes. Variables captured by a closure cannot be garbage-collected while the closure itself is still reachable. An event listener or timer that is never removed keeps everything it closes over alive, including large objects or DOM elements that are no longer on the page. The remedy is to remove listeners and clear timers when they are no longer needed.
Key Points to Remember
- A closure is formed when an inner function retains access to variables from its enclosing (outer) scope after that scope has finished running.
- Closures give a live reference to outer variables, not a copied snapshot, so later changes are reflected inside the closure.
- Closures enable private state, since outer variables are only reachable through the functions that were deliberately returned or exposed.
- Each call to an outer function creates a fresh scope and a fresh, independent closure over its own variables.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Shows a live preview of your HTML, CSS and JavaScript. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering