DEV Community

Cover image for Scope & Closures in JavaScript: The Superpower Hidden Inside Every Function
Shajibul Alam Shihab
Shajibul Alam Shihab

Posted on

Scope & Closures in JavaScript: The Superpower Hidden Inside Every Function

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();
Enter fullscreen mode Exit fullscreen mode

What's happening?

const appName = "My Awesome App";
Enter fullscreen mode Exit fullscreen mode

This variable is created outside every function.

That makes it global.

Now look inside the function.

console.log(appName);
Enter fullscreen mode Exit fullscreen mode

Even though appName wasn't created here, JavaScript finds it because global variables are visible everywhere.

Output

My Awesome App
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

What's happening?

Inside the function:

const username = "Alex";
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

You'll get:

ReferenceError: username is not defined
Enter fullscreen mode Exit fullscreen mode

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:

  • if
  • for
  • while
  • switch

Modern JavaScript lets variables stay inside those blocks.

if (true) {
  let message = "Hello!";
}

// console.log(message);
Enter fullscreen mode Exit fullscreen mode

Output

ReferenceError
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

What's happening?

Inside the block we create two variables.

var leaky = "I escape the block";
Enter fullscreen mode Exit fullscreen mode

and

let contained = "I stay in the block";
Enter fullscreen mode Exit fullscreen mode

After the block ends...

console.log(leaky);
Enter fullscreen mode Exit fullscreen mode

still works.

Output:

I escape the block
Enter fullscreen mode Exit fullscreen mode

But this line:

console.log(contained);
Enter fullscreen mode Exit fullscreen mode

throws a ReferenceError.

Why?

Because:

  • var ignores block boundaries.
  • let respects them.
  • const behaves like let.

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:

  • const by default
  • let when 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());
Enter fullscreen mode Exit fullscreen mode

Step 1: Create a private variable

let count = 0;
Enter fullscreen mode Exit fullscreen mode

Nobody outside this function can touch it directly.


Step 2: Return functions

increment: () => ++count
Enter fullscreen mode Exit fullscreen mode

This function remembers count.

Even after makeCounter() has finished.

The same happens here.

get: () => count
Enter fullscreen mode Exit fullscreen mode

It also remembers count.


Output

0
2
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

Output

2
0
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Many beginners expect:

1
2
3
Enter fullscreen mode Exit fullscreen mode

But the actual output is:

4
4
4
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Output:

1
2
3
Enter fullscreen mode Exit fullscreen mode

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 const unless a value needs to change.
  • Use let when reassignment is necessary.
  • Avoid var in 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 let and const, limits variables to the nearest {}.
  • var is 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)