What are closures?
Closures
A closure is one of the most important - and often misunderstood - ideas in JavaScript.
In simple words, a closure is a function that carries a memory of the environment in which it was created. It remembers the variables that were around it at the time it was defined, even after that outer code has finished executing.
To understand why this happens, you first need to recall that JavaScript uses lexical scope - meaning variable visibility is determined by where a function is written in the code, not by where it is called later.
Because of lexical scope, an inner function automatically "knows" about the variables defined in its outer function.
The backpack analogy - how to imagine a closure
Imagine every function in JavaScript carries an invisible backpack with it.
When the function is created, JavaScript puts into this backpack all the
variables that were in scope at that time - the things the function can "see" from where it was written.
When the outer function finishes running, most of its local variables normally disappear from memory. But if an inner function still references them, those variables stay alive - kept safely inside the backpack.
Whenever the inner function is called later (even long after the outer function is gone), it can still open that backpack and find the variables it remembers.
This is a closure in action: the combination of the function and the preserved environment around it.
- Example
function makeCounter() {
let count = 0; // this variable lives in makeCounter's scope
return function () {
count++; // inner function can still access count
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
What happens here:
makeCounter finishes running after the first call.
Normally, its local variable count would disappear.
But the inner function still "remembers" count through its closure - it keeps it alive inside its backpack.
Every time you call counter(), it finds count in that backpack, updates it, and returns the new value.
Even though makeCounter no longer exists in memory as a running function, the variable count remains because the closure is still holding onto it.
Closures keep references, not copies
It's very important to understand that closures don't store copies of variables - they store references.
That means if a variable's value changes after the closure is created, the closure will see the updated
value.
function outer() {
let message = "Hello";
return function inner() {
console.log(message);
};
}
const greet = outer();
message = "Hi"; // changing outer variable (if accessible)
greet(); // prints "Hello" only if outer scope variable remains unchanged, otherwise references update dynamically
Closures are "live links" to variables, not frozen snapshots. That's why they're so powerful - they can reflect changes over time.
Closures allow data privacy
Before ES6 introduced classes with private fields, closures were the main way to create private data in JavaScript - variables that can't be accessed from outside but are still remembered internally.
function createAccount() {
let balance = 0;
return {
deposit(amount) {
balance += amount;
console.log("Deposited:", amount);
},
getBalance() {
return balance;
},
};
}
const account = createAccount();
account.deposit(100);
console.log(account.getBalance()); // 100
console.log(account.balance); // undefined - cannot access directly
Here, balance acts like a private variable. It lives in the closure of the functions returned by createAccount, and can't be read or modified except through those inner functions.
Closures in asynchronous code
Closures often appear naturally in asynchronous operations like setTimeout, Promise, or event listeners.
The inner function runs later, but still remembers the variables from when it was defined.
function greet(name) {
setTimeout(function () {
console.log("Hello, " + name);
}, 1000);
}
greet("Alice");
Even though greet finishes before one second passes, the callback inside setTimeout still knows what name was - because it carries a closure around it.
Real World uses of closures
Closures aren't just theoretical - they power many common programming patterns:
- Data encapsulation: hiding internal state, like private variables.
- Function factories: creating multiple customized versions of a function.
- Memoization: remembering results for faster future calculations.
- Event handlers and callbacks: retaining access to state across time.
- Modules: grouping related code with private internal data.
// Function Factory
function multiplier(factor) {
return function (n) {
return n * factor;
};
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
Each inner function remembers its own factor - carried in its own backpack from when it was created.
Memory and performance considerations
Closures keep their referenced variables alive in memory as long as they are reachable.
If you store many closures or forget to release them when no longer needed, those variables won't be garbage-collected.
That's not a bug - it's just how JavaScript ensures your closures keep working.
In most normal cases, the runtime cleans up automatically once closures are no longer used.
Summary
A closure is the combination of a function, and the lexical environment where that function was created.
Or in simpler words: A closure is a function plus its backpack of remembered variables. Closures make JavaScript functions powerful and flexible, letting you preserve state, hide data, and control behavior across time - all thanks to lexical scoping and how JavaScript keeps those "backpacks" alive even after the original context is gone.
// private state counter
function makeCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const c1 = makeCounter();
console.log(c1()); // 1
console.log(c1()); // 2
// function factory
function greeter(greeting) {
return function (name) {
return greeting + ", " + name;
};
}
const hello = greeter("Hello");
console.log(hello("Sam")); // Hello, Sam
// async with closure
function delayedLog(msg) {
for (let i = 1; i <= 3; i++) {
setTimeout(() => console.log(msg, i), i * 100);
}
}
delayedLog("Step"); // Step 1, Step 2, Step 3
Common misconceptions
- Closures copy values. They actually keep references to variables, so you observe updated values, not frozen snapshots.
- Closures always cause memory leaks. They only keep what is referenced. If nothing references the inner function, the closure can be collected.
- Using var in loops with callbacks works the same as let. With var, each iteration shares the same variable, which surprises many developers.
Practice questions
- Implement a once utility so a function runs at most one time and returns the same result on later calls.
function once(fn) {
let hasRun = false;
let result;
return function (...args) {
if (!hasRun) {
result = fn.apply(this, args);
hasRun = true;
}
return result;
};
}
- Write a memoize function that caches results by argument for a pure function.
function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
- Explain why var often behaves unexpectedly in loops with asynchronous callbacks and show a fix.
- Var is function scoped.