Have you ever walked into a room, forgotten why you went there, and then suddenly remembered because you saw something familiar?
Our brains are surprisingly good at remembering context.
JavaScript functions can do something very similar.
They can "remember" where they came from even after the place they were created no longer exists.
That sounds almost magical.
But before we get to that magic, we need to understand something much simpler: scope.
Because once scope makes sense, closures stop feeling mysterious and start feeling incredibly useful.
Let's begin with a simple story.
Imagine Your House
Picture your house.
Some things are available to everyone.
The TV in the living room, for example.
Anyone inside the house can use it.
Now think about your bedroom.
Only you have access to what's inside.
Finally, imagine a small drawer inside your desk.
Only someone standing at your desk can open it.
Variables in JavaScript work almost exactly like this.
Some are available everywhere.
Some are only available inside a function.
Some are only available inside a specific block of code.
This idea is called scope.
Scope simply answers one question:
"Where can this variable be accessed?"
Let's explore each type.
Global Scope: The Shared Living Room
Imagine placing a notebook on the living room table.
Everyone in the house can see it.
A variable declared outside every function behaves the same way.
const appName = "My Awesome App";
function greet() {
console.log(appName);
}
greet();
What's happening?
const appName = "My Awesome App";
This variable is created outside every function.
That makes it global.
Now look inside the function.
console.log(appName);
Even though appName wasn't created here, JavaScript finds it because global variables are visible everywhere.
Output
My Awesome App
Why does global scope exist?
Imagine needing your application's name in twenty different places.
Instead of creating twenty copies, you create it once.
Everyone can access it.
Useful?
Absolutely.
Dangerous?
Also yes.
The problem with too many global variables
Imagine ten people all sharing the same whiteboard.
Soon someone accidentally erases another person's notes.
Global variables create the same problem.
Any part of your program can change them.
That's why experienced JavaScript developers try to keep global variables to a minimum.
Real-world example
Global variables are often used for things like:
- Application configuration
- Theme settings
- Environment values
- Constants
Beginner mistake
Creating lots of global variables because they're "easy."
They work...
Until your project grows.
Then finding who changed what becomes frustrating.
Key takeaway
Global scope is shared by the entire program.
It's convenient, but too much sharing creates chaos.
Now let's make things more private.
Function Scope: Your Personal Room
Imagine your bedroom.
You keep your journal there.
Visitors can't see it.
Only you can.
Variables inside a function work exactly like that.
function greetUser() {
const username = "Alex";
console.log(username);
}
greetUser();
// console.log(username);
What's happening?
Inside the function:
const username = "Alex";
The variable belongs only to this function.
JavaScript creates it when the function runs.
When the function finishes, outside code cannot access it.
If you uncomment this line:
console.log(username);
You'll get:
ReferenceError: username is not defined
Why does function scope exist?
Imagine every function sharing every variable.
Names would constantly collide.
One function might accidentally overwrite another function's data.
Function scope prevents that.
Each function gets its own little workspace.
Real-world example
Functions often keep temporary values like:
- Calculations
- User input
- API responses
- Intermediate results
Those values usually aren't useful anywhere else.
Beginner mistake
Trying to use a variable outside the function where it was created.
If JavaScript says "is not defined", the first thing to check is the variable's scope.
Key takeaway
Function scope keeps variables private to the function that created them.
But JavaScript has one more level of privacy...
And it solves a problem developers struggled with for years.
Block Scope: Privacy Inside Curly Braces
Imagine a small locker inside your room.
Not everything in your room belongs in the locker.
Only certain things do.
Blocks ({}) create these little lockers.
Examples include:
ifforwhileswitch
Modern JavaScript lets variables stay inside those blocks.
if (true) {
let message = "Hello!";
}
// console.log(message);
Output
ReferenceError
The variable exists only inside the block.
Simple.
But something interesting happens when we replace let with var.
Why var Behaves Differently
Let's try this.
function example() {
if (true) {
var leaky = "I escape the block";
let contained = "I stay in the block";
}
console.log(leaky);
// console.log(contained);
}
example();
What's happening?
Inside the block we create two variables.
var leaky = "I escape the block";
and
let contained = "I stay in the block";
After the block ends...
console.log(leaky);
still works.
Output:
I escape the block
But this line:
console.log(contained);
throws a ReferenceError.
Why?
Because:
-
varignores block boundaries. -
letrespects them. -
constbehaves likelet.
This confusing behavior is one of the biggest reasons modern JavaScript prefers let and const.
Real-world example
Imagine writing a loop.
With let, each loop iteration gets its own private variable.
With var, every iteration shares the same one.
That tiny difference prevents countless bugs.
Beginner mistake
Using var simply because old tutorials still teach it.
Unless you're maintaining legacy code, prefer:
-
constby default -
letwhen the value changes
Key takeaway
let and const make scope match what your eyes already see.
If a variable is inside braces, it stays inside those braces.
Now that we know where variables live...
Here's the big question.
What happens when a function leaves its home?
Does it forget everything?
Surprisingly...
No.
Meet Closures: Functions That Remember
Imagine you're moving to another city.
Before leaving, your friend gives you a notebook filled with important phone numbers.
Years later...
You still have it.
Even though you've left.
Closures work almost exactly like that.
A function remembers variables from where it was created.
Even after the outer function has already finished running.
Let's see it.
function makeCounter() {
let count = 0;
return {
increment: () => ++count,
get: () => count,
};
}
const counter = makeCounter();
console.log(counter.get());
counter.increment();
counter.increment();
console.log(counter.get());
Step 1: Create a private variable
let count = 0;
Nobody outside this function can touch it directly.
Step 2: Return functions
increment: () => ++count
This function remembers count.
Even after makeCounter() has finished.
The same happens here.
get: () => count
It also remembers count.
Output
0
2
Even though makeCounter() finished long ago...
The returned functions still know what count is.
That's a closure.
Why Do Closures Exist?
Imagine you want a variable nobody else can change.
Without closures, you'd probably make it global.
That would let every part of your program modify it.
Closures solve this beautifully.
The variable stays private.
Only the returned functions can access it.
It's like locking something in a safe and handing out only the keys you choose.
Every Closure Gets Its Own Memory
Now let's create two counters.
const counterA = makeCounter();
const counterB = makeCounter();
counterA.increment();
counterA.increment();
console.log(counterA.get());
console.log(counterB.get());
Output
2
0
Why?
Because every call to makeCounter() creates a brand-new private count.
Think of it like baking cookies.
Each batch gets its own bowl.
Changing one bowl doesn't affect another.
Real-world uses for closures
Closures appear everywhere in JavaScript, often without you realizing it.
They're commonly used for:
- Private state
- Counters
- Caches
- Memoized functions
- Event handlers
- Callbacks
- Factory functions
Whenever a function needs to remember information for later, a closure is usually involved.
The Famous var Loop Bug
There's one closure mistake almost every JavaScript developer encounters.
Consider this code.
for (var i = 1; i <= 3; i++) {
setTimeout(() => {
console.log(i);
}, 100);
}
Many beginners expect:
1
2
3
But the actual output is:
4
4
4
Why?
Every callback closes over the same i.
By the time the timers run, the loop has already finished and i is 4.
Now replace var with let.
for (let i = 1; i <= 3; i++) {
setTimeout(() => {
console.log(i);
}, 100);
}
Output:
1
2
3
Each iteration gets its own separate i.
This is one of the reasons let became such an important addition to JavaScript.
Do Closures Keep Variables Alive?
Here's something many beginners don't expect.
When a function finishes, its local variables are usually cleaned up.
But closures change the story.
If another function still references those variables, JavaScript keeps them in memory.
Not because it forgot to clean them up.
Because they're still being used.
Once nothing references the closure anymore, JavaScript is free to reclaim that memory.
It's a smart balance between preserving useful data and cleaning up what is no longer needed.
Scope at a Glance
| Scope Type | Visible Where? | Common Use |
|---|---|---|
| Global | Everywhere | App-wide configuration and constants |
| Function | Inside one function | Temporary calculations and local data |
| Block | Inside {} only |
Loops, conditions, and temporary values |
| Closure | Remembered from outer scope | Private state, counters, callbacks, caches |
Best Practices
A few habits will save you from many frustrating bugs:
- Prefer
constunless a value needs to change. - Use
letwhen reassignment is necessary. - Avoid
varin modern JavaScript. - Keep global variables to a minimum.
- Use closures intentionally for private state instead of relying on globals.
- Be mindful that closures keep referenced variables alive as long as they're still needed.
Key Takeaways
- Scope determines where a variable can be accessed.
- Global variables are visible everywhere, but too many can make code harder to maintain.
- Function scope keeps variables private to a function.
- Block scope, created by
letandconst, limits variables to the nearest{}. -
varis function-scoped, which can lead to unexpected behavior in blocks and loops. - Closures allow functions to remember variables from the scope where they were created.
- Every call to an outer function creates a fresh closure with its own independent state.
- Closures power many everyday JavaScript patterns, from counters to callbacks and event handlers.
What's Next?
Now that you know how JavaScript decides where variables live and how functions remember them, you're ready for one of the most exciting parts of the language.
Functions don't just remember data.
They can also be passed around, stored in variables, returned from other functions, and treated like any other value.
That simple idea unlocks callbacks, promises, asynchronous programming, and many of the patterns you'll see in modern JavaScript frameworks.
And once you recognize closures working quietly behind the scenes, you'll start seeing them everywhere.
That's when JavaScript stops feeling like a collection of syntax rules and starts feeling like a language with a surprisingly elegant design.
Top comments (0)