JavaScript Closures Deep Dive: Complete Guide
A closure is a function that remembers the variables from the scope where it was created, even after that scope is gone. Learn how closures work, why they matter, and what you can build with them.
A closure is a function combined with the variables from the scope where it was created. The function "remembers" those variables even after the outer scope has finished executing. Closures are not a special syntax or a keyword. They are the natural result of two JavaScript features working together: lexical scope and functions as first-class values.
Here is the simplest closure:
function createGreeter(greeting) {
return function(name) {
return `${greeting}, ${name}!`; // greeting is from the outer scope
};
}
const sayHello = createGreeter("Hello");
console.log(sayHello("Alex")); // "Hello, Alex!"createGreeter runs, receives "Hello", and returns the inner function. Then createGreeter finishes, and its local variables should be gone.
But sayHello still remembers a greeting of "Hello". That remembered connection is a closure.
Why Closures Work
Closures work because of JavaScript's scope chain. When a function is created, it stores a reference to the scope it was born in. Even when that scope is no longer active, the reference keeps the variables alive:
The outer scope would normally be garbage collected. But because the inner function still references it, the variables persist. Each call to the outer function creates a fresh scope and a fresh closure over it.
The Counter Example: Why Each Closure Is Independent
A closure is created fresh on every call to the outer function, not once for the whole program. This counter factory makes that independence visible:
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}Creating two separate counters and interleaving calls to each one shows that they never interfere with each other, since each one closes over its own private count variable:
const counterA = createCounter();
const counterB = createCounter();
console.log(counterA()); // 1
console.log(counterA()); // 2
console.log(counterB()); // 1 (independent!)
console.log(counterA()); // 3Every call to createCounter creates a NEW count variable and a NEW closure. The two counters do not share state. This is the key reason closures are useful for data privacy, each closure has its own private variables that no other code can touch.
Data Privacy with Closures
Closures are JavaScript's primary mechanism for private state without classes. Each method below closes over the same private balance variable, and that variable has no name anywhere outside this function:
function createBankAccount(initialBalance) {
let balance = initialBalance; // Private variable
return {
deposit(amount) { balance += amount; return balance; },
withdraw(amount) {
if (amount > balance) return "Insufficient funds";
balance -= amount;
return balance;
},
getBalance() { return balance; },
};
}Using the account only through its methods keeps every read and write funneled through the same controlled surface, since balance itself is never exposed as a property:
const account = createBankAccount(100);
console.log(account.getBalance()); // 100
console.log(account.deposit(50)); // 150
console.log(account.withdraw(30)); // 120
console.log(account.balance); // undefined -- cannot access directlybalance is completely private. The only way to read or change it is through the returned methods. This pattern, an outer function that returns an object of methods that close over private state, is called the module pattern.
Closures in Loops: The Classic Trap
// Bug: all callbacks share the same i
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3
// Fix with let: each iteration gets its own i
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2With var, there is one i for the whole function. All three callbacks close over the same i, which is 3 by the time they run. With let, each loop iteration creates a new i, so each callback closes over its own value.
If you must use var, the old fix was an IIFE to capture each value:
for (var i = 0; i < 3; i++) {
((j) => {
setTimeout(() => console.log(j), 100);
})(i);
}
// Prints: 0, 1, 2Closures and the Event Loop
Closures are what make asynchronous callbacks useful. When you pass a callback to setTimeout or addEventListener, the callback remembers the scope where it was created:
function setupButton(buttonId, message) {
const button = document.getElementById(buttonId);
button.addEventListener("click", function() {
// This callback closes over message
alert(message);
});
}
setupButton("btn1", "You clicked button 1!");
setupButton("btn2", "You clicked button 2!");Each call to setupButton creates a closure that remembers its own message. The event listener runs long after setupButton has returned, but message is still available.
Practical: Once Function
A closure can track whether a function has been called, using a private flag that only the returned function can see:
function once(fn) {
let called = false;
return function(...args) {
if (!called) {
called = true;
return fn(...args);
}
};
}Calling the wrapped function repeatedly only ever runs the original logic on the first attempt, since every call after that finds the flag already set and returns nothing:
const initialize = once(() => console.log("Initialized!"));
initialize(); // "Initialized!"
initialize(); // (nothing)
initialize(); // (nothing)The returned function closes over called. After the first invocation, called is true and all subsequent calls are ignored.
Practical: Memoization
Closures can cache function results, storing each computed answer the first time and reusing it on every later call with the same input:
function memoize(fn) {
const cache = {};
return function(arg) {
if (arg in cache) {
return cache[arg];
}
const result = fn(arg);
cache[arg] = result;
return result;
};
}Calling the memoized function twice with the same argument only computes the result once, since the second call finds the answer already sitting in the cache:
const memoizedSquare = memoize((n) => {
console.log("Computing...");
return n * n;
});
console.log(memoizedSquare(5)); // Computing... 25
console.log(memoizedSquare(5)); // 25 (from cache, no Computing...)The cache object is private to the closure. No external code can access or corrupt it.
For more closure patterns, see Practical Use Cases for JS Closures in Real Apps. For how to avoid memory issues, see How to Prevent Memory Leaks in JavaScript Closures.
Rune AI
Key Insights
- A closure is created when a function accesses variables from an outer scope and survives beyond that scope.
- Closures are the mechanism behind data privacy, function factories, and module patterns.
- Each call to a factory function creates a new closure with independent state.
- Closures keep outer variables alive. Release references when you are done to avoid memory leaks.
- Closures are not a special feature. They are the natural result of lexical scope plus functions as values.
Frequently Asked Questions
Are closures unique to JavaScript?
Do closures cause memory leaks?
How do I know if I have created a closure?
Conclusion
A closure is a function plus its remembered environment. It is not a special syntax or keyword. It is the natural result of lexical scope combined with functions being first-class values. Closures give you data privacy, stateful functions, and the foundation for nearly every advanced JavaScript pattern. Once you internalize that a function carries its birth scope with it forever, closures stop being mysterious and start being useful.
More in this topic
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.
How to Loop Through Arrays Using JS for Loops Guide
Looping through arrays is the most practical use of JavaScript for loops. Learn how to access each element by index, avoid off-by-one errors, and choose between for, for...of, and forEach.