Closures Aren’t Magic, They’re Just Lexical Environments That Refuse to Die
Every JavaScript developer eventually hears this sentence:
“A closure is a function bundled together with its lexical environment.”
Everyone memorizes it.
Few people actually understand what it means.
Many developers can define a closure, but when asked where it’s stored, when it’s created, or why JavaScript even needs it, the explanation usually falls apart.
Closures aren’t magic.
They’re simply a consequence of how JavaScript manages execution contexts and lexical environments.
Let’s look under the hood.
The Common Misconception
Many developers imagine something like this:
function outer() {
let count = 0;
return function () {
count++;
console.log(count);
};
}And then they say:
“The inner function remembers count.”
Sure.
But…
How?
After outer() finishes, shouldn't count disappear?
Normally, yes.
Local variables are supposed to die when their function returns.
So why doesn’t this one?
Let’s Follow What the JavaScript Engine Actually Does
When you call:
const increment = outer();the engine creates an Execution Context for outer().
That execution context contains:
Execution Context
------------------------
Variable Environment
Lexical Environment
this
Outer ReferenceInside the Lexical Environment:
Lexical Environment
count → 0
increment function → memory referenceEverything seems normal so far.
Then outer() Returns…
Normally JavaScript removes the execution context from the call stack.
Call Stack
Global
outer() ← popped offAt this point you might expect:
countto disappear.
But it doesn’t.
Why?
Because something still needs it.
The Hidden Reference
The returned function doesn’t just contain executable code.
It also stores an internal reference.
Conceptually:
Function Object
Code
[[Environment]] → Lexical Environment of outer()That internal reference is what people call a closure.
It isn’t copying variables.
It isn’t cloning objects.
It simply says:
“If I ever need a variable that isn’t inside me,
go look over there.”
Nothing Gets Copied
This surprises many.
Consider:
function outer() {
let count = 0;
return function () {
count++;
console.log(count);
};
}
const increment = outer();
increment();
increment();
increment();Output:
1
2
3If closures copied variables…
every call would print:
1Instead, the same variable is being updated.
The closure references the original lexical environment.
Where Is That Lexical Environment Stored?
A common interview question:
Is it on the stack?
No.
The execution context was on the stack.
But the lexical environment survives because something still references it.
JavaScript engines promote that environment to managed memory (the heap), where the garbage collector can keep it alive.
Think of it like this:
Stack
Global
↓
Heap
Lexical Environment
count = 3
↑
Function
[[Environment]]The stack frame disappears.
Get Ramya Selvaraj’s stories in your inbox
Join Medium for free to get updates from this writer.
The environment doesn’t.
Why JavaScript Needs Closures
Imagine closures didn’t exist.
function createCounter() {
let count = 0;
return function () {
count++;
return count;
};
}After createCounter() finishes...
count would disappear.
Every call would fail.
Closures make private state possible.
Without them:
- React Hooks wouldn’t work.
- Event handlers couldn’t access surrounding variables.
- Function factories wouldn’t exist.
- Memoization would be impossible.
- Debouncing and throttling would break.
Closures are one of JavaScript’s foundational features — not just a convenience.
Closures Are Created Immediately
Another common misunderstanding:
“Closures are created when the function executes.”
Not exactly.
They’re created when the function is defined, because that’s when JavaScript records the function’s surrounding lexical environment.
Example:
function outer() {
let message = "Hello";
const sayHello = () => {
console.log(message);
};
return sayHello;
}The arrow function immediately records:
[[Environment]]
↓
outer lexical environmentEven before it’s called.
React Uses Closures Everywhere
Consider this component:
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
console.log(count);
}
return (
<button onClick={handleClick}>
Click
</button>
);
}handleClick closes over count.
Every render creates a new closure.
That’s why stale closure bugs happen.
For example:
useEffect(() => {
const id = setInterval(() => {
console.log(count);
}, 1000);
}, []);Developers expect:
0
1
2
3Instead they get:
0
0
0
0Why?
Because the interval captured the lexical environment from the first render.
The callback isn’t broken.
The closure is doing exactly what it was designed to do.
Can Closures Cause Memory Leaks?
Yes.
Not because closures are bad.
Because references prevent garbage collection.
Example:
function createBigObject() {
const hugeArray = new Array(1000000).fill("data");
return function () {
console.log(hugeArray.length);
};
}
const fn = createBigObject();Even though createBigObject() finished...
hugeArray cannot be collected.
The closure still references it.
Once:
fn = null;(or all references to the function are removed)
the lexical environment becomes unreachable.
The garbage collector can reclaim the memory.
Closures themselves don’t leak memory — unnecessary long-lived references do.
The Mental Model That Finally Makes Closures Click
Instead of memorizing definitions…
Picture this:
Function
↓
Needs variable?
↓
Follow [[Environment]]
↓
Lexical Environment
↓
Find variable
↓
Use itThat’s all.
No magic.
No copying.
No hidden storage tricks.
Just a function carrying a pointer to the scope where it was created.
Final Thoughts
Closures are often presented as one of JavaScript’s most mysterious features.
In reality, they’re a natural outcome of lexical scoping.
A function doesn’t “remember” variables through magic. It simply retains a reference to the lexical environment in which it was created. As long as that reference exists, the environment stays alive. When nothing references it anymore, the garbage collector cleans it up.
Once you understand that mental model, closures stop feeling like a special language feature and start feeling like a predictable part of JavaScript’s execution model.
And that’s when many other concepts — React Hooks, event handlers, function factories, memoization, and even stale closure bugs — become much easier to reason about.







