DEV Community

Cover image for JavaScript Closures — The Complete Guide (From Beginner to Advanced)
Abanoub Kerols
Abanoub Kerols

Posted on

JavaScript Closures — The Complete Guide (From Beginner to Advanced)

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

  1. What is a Closure?
  2. Why Closures Exist
  3. JavaScript Functions are First-Class Objects
  4. Lexical Scope
  5. Scope Chain
  6. Execution Context
  7. The Call Stack
  8. Functions Remember Their Environment
  9. The Backpack Analogy
  10. Closed Over Variables
  11. Returning Functions
  12. Persistent State
  13. Multiple Independent Closures
  14. Real World Examples
  15. Once Function
  16. Memoization
  17. Module Pattern
  18. Common Mistakes
  19. Performance Considerations
  20. Best Practices
  21. 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);
Enter fullscreen mode Exit fullscreen mode

Output

ReferenceError
Enter fullscreen mode Exit fullscreen mode

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

Output

Hello
Enter fullscreen mode Exit fullscreen mode

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

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

Output

JavaScript
Enter fullscreen mode Exit fullscreen mode

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

Output

20
Enter fullscreen mode Exit fullscreen mode

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

Example

let country = "Egypt";

function one() {

    function two() {

        function three() {
            console.log(country);
        }

        three();
    }

    two();
}

one();
Enter fullscreen mode Exit fullscreen mode

JavaScript searches:

three()

↓

two()

↓

one()

↓

Global
Enter fullscreen mode Exit fullscreen mode

Eventually finds

country
Enter fullscreen mode Exit fullscreen mode

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

Execution Context

multiply

a = 5

b = 6

result = 30
Enter fullscreen mode Exit fullscreen mode

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

Call Stack

three()

two()

one()

Global
Enter fullscreen mode Exit fullscreen mode

After finishing

Global
Enter fullscreen mode Exit fullscreen mode

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

Output

Hello
Enter fullscreen mode Exit fullscreen mode

Even though

createGreeting()
Enter fullscreen mode Exit fullscreen mode

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);

    };

}
Enter fullscreen mode Exit fullscreen mode

Backpack

count = 0
Enter fullscreen mode Exit fullscreen mode

Every time the function executes

count
Enter fullscreen mode Exit fullscreen mode

is still there.

Output

1

2

3

4
Enter fullscreen mode Exit fullscreen mode

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;

    };

}
Enter fullscreen mode Exit fullscreen mode

Here

value
Enter fullscreen mode Exit fullscreen mode

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

Output

16

24
Enter fullscreen mode Exit fullscreen mode

Each returned function remembers its own value of x.


Persistent State

Without Closure

let count = 0;

function increment() {

    count++;

}
Enter fullscreen mode Exit fullscreen mode

Global variables are dangerous.

With Closure

function createCounter() {

    let count = 0;

    return function() {

        count++;

        return count;

    };

}
Enter fullscreen mode Exit fullscreen mode

Everything becomes private.

No global pollution.

Much safer.


Multiple Independent Closures

Every call creates a completely independent closure.

const counterA = createCounter();

const counterB = createCounter();
Enter fullscreen mode Exit fullscreen mode

Memory

counterA

count = 0
Enter fullscreen mode Exit fullscreen mode

Memory

counterB

count = 0
Enter fullscreen mode Exit fullscreen mode

Now

console.log(counterA());

console.log(counterA());

console.log(counterB());

console.log(counterB());

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

Output

1

2

1

2

3
Enter fullscreen mode Exit fullscreen mode

Each function owns its own memory.

This is one of the most important concepts in closures.

Top comments (0)