One of the most powerful features in JavaScript isn't a framework, a library, or even a language keyword... it's Closures.
Closures are everywhere.
Every React Hook, every Express middleware, every event listener, every callback, every memoization function, every module pattern, and many advanced JavaScript libraries rely heavily on closures.
Yet, many developers memorize the definition without truly understanding what happens behind the scenes.
In this guide, we'll build an intuitive understanding of JavaScript Closures from scratch, explore how JavaScript stores variables, understand lexical scope, dive into execution contexts, and finally build production-ready examples used by professional software engineers.
Table of Contents
- What is a Closure?
- Why Closures Exist
- JavaScript Functions are First-Class Objects
- Lexical Scope
- Scope Chain
- Execution Context
- The Call Stack
- Functions Remember Their Environment
- The Backpack Analogy
- Closed Over Variables
- Returning Functions
- Persistent State
- Multiple Independent Closures
- Real World Examples
- Once Function
- Memoization
- Module Pattern
- Common Mistakes
- Performance Considerations
- Best Practices
- Conclusion
What is a Closure?
The official definition from MDN says:
A closure is a function bundled together with references to its surrounding lexical environment.
Although technically correct...
It doesn't really explain what is happening.
A much simpler definition is:
A Closure is a function that remembers the variables that existed when it was created—even after the outer function has already finished executing.
That's it.
The keyword here is remembers.
Why Do Closures Exist?
Normally, local variables disappear after a function finishes.
Example:
function hello() {
let message = "Hello";
}
hello();
// message no longer exists
console.log(message);
Output
ReferenceError
The variable lived only inside hello().
After execution finished...
Memory should have been released.
So why does this work?
function outer() {
let message = "Hello";
function inner() {
console.log(message);
}
return inner;
}
const fn = outer();
fn();
Output
Hello
How?
The outer function already finished.
Its variables should have disappeared.
But they didn't.
Because of Closure.
JavaScript Functions are More Than Code
Many developers think a function is only executable code.
Actually, a JavaScript function is an object.
It contains:
- executable code
- name
- parameters
- internal properties
- reference to where it was created
Conceptually:
Function Object
{
code: ...
name: "inner"
[[Environment]] ---> Outer Scope
}
That hidden reference is the secret behind closures.
Lexical Scope
Closures cannot exist without understanding lexical scope.
Lexical means:
Determined by where the code is written.
Example
let language = "JavaScript";
function printLanguage() {
console.log(language);
}
printLanguage();
Output
JavaScript
The function looks upward through its surrounding scopes.
Not where it is called.
Where it is defined.
Example:
let value = 10;
function outer() {
let value = 20;
function inner() {
console.log(value);
}
return inner;
}
const fn = outer();
fn();
Output
20
Not 10.
Because inner() was defined inside outer().
Scope Chain
Whenever JavaScript cannot find a variable...
It walks upward.
Current Scope
↓
Outer Scope
↓
Global Scope
↓
null
Example
let country = "Egypt";
function one() {
function two() {
function three() {
console.log(country);
}
three();
}
two();
}
one();
JavaScript searches:
three()
↓
two()
↓
one()
↓
Global
Eventually finds
country
Execution Context
Every function call creates an Execution Context.
It contains
- Variables
- Parameters
- this
- Scope Reference
Example
function multiply(a, b) {
let result = a * b;
return result;
}
multiply(5, 6);
Execution Context
multiply
a = 5
b = 6
result = 30
Normally...
After the function returns...
Everything disappears.
Except...
If another function still needs it.
That's Closure.
The Call Stack
Consider
function one() {
two();
}
function two() {
three();
}
function three() {
console.log("Done");
}
one();
Call Stack
three()
two()
one()
Global
After finishing
Global
Everything pops off the stack.
Normally memory disappears.
Closures change this behavior.
Functions Have Permanent Memories
Imagine this code.
function createGreeting() {
let greeting = "Hello";
return function () {
console.log(greeting);
};
}
const greet = createGreeting();
greet();
Output
Hello
Even though
createGreeting()
already finished...
Its variable still exists.
Why?
Because the returned function still references it.
The Backpack Analogy
One of the best mental models for closures is the Backpack.
Imagine every function carries a backpack.
Inside the backpack...
JavaScript stores every variable the function might need later.
Example
function outer() {
let count = 0;
return function () {
count++;
console.log(count);
};
}
Backpack
count = 0
Every time the function executes
count
is still there.
Output
1
2
3
4
The variable never resets because it lives inside the closure.
Closed Over Variables
Variables preserved by closures are called
Closed Over Variables
Example
function counter() {
let value = 0;
return function () {
value++;
return value;
};
}
Here
value
is a closed over variable.
It survives every function call.
Returning Functions
Closures become useful when returning functions.
function multiplyBy(x) {
return function(y) {
return x * y;
};
}
const double = multiplyBy(2);
const triple = multiplyBy(3);
console.log(double(8));
console.log(triple(8));
Output
16
24
Each returned function remembers its own value of x.
Persistent State
Without Closure
let count = 0;
function increment() {
count++;
}
Global variables are dangerous.
With Closure
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
Everything becomes private.
No global pollution.
Much safer.
Multiple Independent Closures
Every call creates a completely independent closure.
const counterA = createCounter();
const counterB = createCounter();
Memory
counterA
count = 0
Memory
counterB
count = 0
Now
console.log(counterA());
console.log(counterA());
console.log(counterB());
console.log(counterB());
console.log(counterA());
Output
1
2
1
2
3
Each function owns its own memory.
This is one of the most important concepts in closures.

Top comments (0)