Sitemap

Closures Aren’t Magic, They’re Just Lexical Environments That Refuse to Die

4 min readAug 2, 2026

--

Press enter or click to view image in full size
closure

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 Reference

Inside the Lexical Environment:

Lexical Environment

count → 0

increment function → memory reference

Everything seems normal so far.

Then outer() Returns…

Normally JavaScript removes the execution context from the call stack.

Call Stack

Global

outer() ← popped off

At this point you might expect:

count

to 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
3

If closures copied variables…

every call would print:

1

Instead, 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 environment

Even 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
3

Instead they get:

0
0
0
0

Why?

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 it

That’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.

--

--