How JavaScript Closures Really Work

How JavaScript Closures Work with Examples 

User avatar placeholder
Written by Amir58

February 15, 2026

Hey there! Have you ever been coding in JavaScript and suddenly come across the term “closure”? Maybe it made you scratch your head a little. Don’t worry, you’re not alone. Closures are one of those concepts that sound super complicated at first, but once you break them down, they actually make a lot of sense. And guess what? You might have already used them without even realizing it.

In this article, we’re going to sit down together and really understand how JavaScript closures work. I promise to keep things simple, use plenty of examples, and avoid any confusing jargon. Think of this as a friendly chat where we unravel this topic step by step. By the end, you’ll feel confident explaining closures to someone else. Let’s dive in!


What Exactly is a Closure?

Let’s start with the basics. A closure is a feature in JavaScript where an inner function has access to the outer (enclosing) function’s variables. It’s like the inner function remembers the environment it was created in, even after the outer function has finished running.

Imagine you have a backpack. You pack some items inside it and then walk around. Even when you’re far from home, you still have those items with you. In this analogy, the backpack is the closure. It carries the variables from the outer function’s scope wherever the inner function goes.

A Simple Example to Get Started

Let’s look at a basic example:

function outerFunction(outerVariable) {
    return function innerFunction(innerVariable) {
        console.log('Outer Variable: ' + outerVariable);
        console.log('Inner Variable: ' + innerVariable);
    };
}

const newFunction = outerFunction('outside');
newFunction('inside');

If you run this code, you’ll see:

Outer Variable: outside
Inner Variable: inside

See what happened? The innerFunction still remembers the outerVariable even after outerFunction has finished executing. That’s a closure in action!


Why Do We Need Closures?

Now you might be wondering, “That’s cool, but why should I care?” Great question! Closures are incredibly useful in JavaScript for several reasons:

1. Data Privacy and Encapsulation

Closures let us create private variables that can’t be accessed from outside. This is super helpful when you want to hide implementation details.

2. Creating Function Factories

We can use closures to generate functions with specific behaviors. Think of it like a function that makes other functions!

3. Maintaining State in Callbacks

When working with asynchronous code, closures help us remember values between function calls.

4. Functional Programming Patterns

Many functional programming techniques rely on closures, like currying and partial application.

Let’s explore each of these with real examples.


How Closures Work Under the Hood

To really understand closures, we need to peek at how JavaScript handles scope and memory. Don’t worry, I’ll keep it simple!

Lexical Scoping

JavaScript uses something called lexical scoping. This means that the scope of a variable is determined by its position in the code. In other words, where you write a function matters.

let globalVar = "I'm global";

function parent() {
    let parentVar = "I'm in the parent";

    function child() {
        let childVar = "I'm in the child";
        console.log(globalVar);  // Can access
        console.log(parentVar);  // Can access
        console.log(childVar);   // Can access
    }

    child();
    // console.log(childVar);  // Error! Can't access
}

parent();

When a function is defined, it carries its lexical environment with it. This environment includes any variables that were in scope at the time.

The Scope Chain

Every function in JavaScript has a scope chain. When you try to access a variable, JavaScript looks:

  1. In the current function’s local scope
  2. Then in the outer function’s scope
  3. Then further out until it reaches the global scope

Closures keep this scope chain alive even after the outer function returns.

Memory and Garbage Collection

Here’s something interesting: Normally, when a function finishes running, JavaScript’s garbage collector cleans up its variables to free memory. But when we have a closure, JavaScript says, “Wait, someone still needs these variables!” So it keeps them alive.

function counter() {
    let count = 0;

    return function() {
        count++;
        return count;
    };
}

const myCounter = counter();
console.log(myCounter()); // 1
console.log(myCounter()); // 2
console.log(myCounter()); // 3

The count variable isn’t destroyed because the inner function still references it. It’s like a little secret that only that function knows!


Practical Examples of Closures

Let’s build some real-world examples to see closures in action.

Example 1: Creating a Simple Counter

Remember our counter example? Let’s expand it to show how we can create multiple independent counters:

function createCounter(startFrom = 0) {
    let count = startFrom;

    return {
        increment() {
            count++;
            return count;
        },
        decrement() {
            count--;
            return count;
        },
        reset() {
            count = startFrom;
            return count;
        },
        getCount() {
            return count;
        }
    };
}

const counter1 = createCounter(5);
const counter2 = createCounter(10);

console.log(counter1.increment()); // 6
console.log(counter1.increment()); // 7
console.log(counter2.increment()); // 11
console.log(counter1.reset());     // 5
console.log(counter2.getCount());  // 11

Each counter has its own private count variable. They don’t interfere with each other!

Example 2: Function Factory for Greetings

Let’s make a function that creates personalized greeting functions:

function greetGenerator(greeting) {
    return function(name) {
        console.log(`${greeting}, ${name}!`);
    };
}

const sayHello = greetGenerator('Hello');
const sayHi = greetGenerator('Hi');
const sayHowdy = greetGenerator('Howdy');

sayHello('Alice');   // Hello, Alice!
sayHi('Bob');        // Hi, Bob!
sayHowdy('Charlie'); // Howdy, Charlie!

Each greeting function remembers its specific greeting word. Pretty neat, right?

Example 3: Private Variables in Objects

Closures help us create truly private variables that can’t be accessed directly:

function createBankAccount(initialBalance) {
    let balance = initialBalance;

    return {
        deposit(amount) {
            if (amount > 0) {
                balance += amount;
                console.log(`Deposited $${amount}. New balance: $${balance}`);
            }
        },
        withdraw(amount) {
            if (amount > 0 && amount <= balance) {
                balance -= amount;
                console.log(`Withdrew $${amount}. New balance: $${balance}`);
            } else {
                console.log('Insufficient funds!');
            }
        },
        checkBalance() {
            console.log(`Current balance: $${balance}`);
        }
    };
}

const myAccount = createBankAccount(100);
myAccount.deposit(50);   // Deposited $50. New balance: $150
myAccount.withdraw(30);  // Withdrew $30. New balance: $120
console.log(myAccount.balance); // undefined - can't access directly!
myAccount.checkBalance(); // Current balance: $120

The balance variable is completely private. No one can change it except through the methods we provide!

Example 4: Handling Asynchronous Operations

Closures are super useful with setTimeout, event handlers, and other async code:

function delayedMessage(message, delay) {
    setTimeout(function() {
        console.log(message);
    }, delay);
}

delayedMessage('Hello after 2 seconds!', 2000);

// But here's a more interesting example:
function createTimer() {
    let startTime = Date.now();

    return function() {
        let elapsed = Date.now() - startTime;
        console.log(`Elapsed time: ${elapsed}ms`);
    };
}

const showElapsed = createTimer();
// Do some work...
setTimeout(() => showElapsed(), 1000); // Elapsed time: about 1000ms
setTimeout(() => showElapsed(), 2000); // Elapsed time: about 2000ms

The timer function remembers when it was created and can always tell you how much time has passed!


Common Pitfalls and How to Avoid Them

Closures are powerful, but they can also trip you up if you’re not careful. Let’s look at some common mistakes.

The Loop Variable Problem

This is probably the most famous closure gotcha:

// Problematic code
for (var i = 1; i <= 3; i++) {
    setTimeout(function() {
        console.log('Count: ' + i);
    }, 1000);
}
// Output: Count: 4 (three times!)

Wait, what happened? We expected 1, 2, 3 but got 4 three times! This happens because:

  1. var is function-scoped, not block-scoped
  2. By the time the setTimeout runs, the loop has finished and i is 4
  3. All the callback functions share the same i

Solution 1: Use let instead of var

for (let i = 1; i <= 3; i++) {
    setTimeout(function() {
        console.log('Count: ' + i);
    }, 1000);
}
// Output: 1, 2, 3

let creates a new binding for each iteration, so each callback gets its own i.

Solution 2: Create a closure using an IIFE

for (var i = 1; i <= 3; i++) {
    (function(j) {
        setTimeout(function() {
            console.log('Count: ' + j);
        }, 1000);
    })(i);
}
// Output: 1, 2, 3

The IIFE (Immediately Invoked Function Expression) captures the current value of i as j in its own scope.

Memory Leaks

Because closures keep variables alive, they can sometimes cause memory leaks if you’re not careful:

function setupHandler() {
    let largeData = new Array(1000000).fill('some data');

    document.getElementById('myButton').addEventListener('click', function() {
        console.log('Button clicked!');
        // This function closes over largeData, keeping it in memory
    });
}

Even though the click handler doesn’t use largeData, it still holds a reference to it through the closure. The fix is to avoid closing over large data you don’t need, or to null out references when done.


Advanced Closure Patterns

Ready to level up? Let’s look at some more sophisticated uses of closures.

Currying

Currying transforms a function that takes multiple arguments into a sequence of functions, each taking a single argument:

function multiply(a) {
    return function(b) {
        return a * b;
    };
}

const multiplyByTwo = multiply(2);
const multiplyByThree = multiply(3);

console.log(multiplyByTwo(5));  // 10
console.log(multiplyByThree(5)); // 15
console.log(multiply(4)(6));     // 24

Memoization (Caching)

We can use closures to cache expensive function results:

function memoize(fn) {
    const cache = {};

    return function(arg) {
        if (cache[arg] !== undefined) {
            console.log('Returning from cache');
            return cache[arg];
        }

        console.log('Computing result');
        const result = fn(arg);
        cache[arg] = result;
        return result;
    };
}

function slowSquare(n) {
    // Simulate slow computation
    for (let i = 0; i < 1000000000; i++) {}
    return n * n;
}

const memoizedSquare = memoize(slowSquare);

console.log(memoizedSquare(5)); // Computing result, then 25
console.log(memoizedSquare(5)); // Returning from cache, then 25
console.log(memoizedSquare(6)); // Computing result, then 36

Module Pattern

Before ES6 modules, closures were used to create modules with private and public parts:

const myModule = (function() {
    // Private variables and functions
    let privateCount = 0;

    function privateLog(message) {
        console.log(`[Private Log]: ${message}`);
    }

    // Public API
    return {
        increment() {
            privateCount++;
            privateLog(`Count incremented to ${privateCount}`);
        },
        decrement() {
            privateCount--;
            privateLog(`Count decremented to ${privateCount}`);
        },
        getCount() {
            return privateCount;
        }
    };
})();

myModule.increment(); // [Private Log]: Count incremented to 1
myModule.increment(); // [Private Log]: Count incremented to 2
console.log(myModule.getCount()); // 2
// console.log(myModule.privateCount); // undefined

Closures in Popular JavaScript Libraries

You’ve probably used closures without knowing it if you’ve worked with popular libraries. Here are a few examples:

React Hooks

React’s useState and useEffect rely on closures behind the scenes:

function Counter() {
    const [count, setCount] = useState(0);

    useEffect(() => {
        // This effect "closes over" count
        document.title = `You clicked ${count} times`;
    });

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>
                Click me
            </button>
        </div>
    );
}

jQuery

Many jQuery methods use closures for event handling and callbacks:

$('#myButton').on('click', function() {
    // This function closes over whatever variables were in scope
    console.log('Button clicked!');
});

Node.js

In Node.js, closures are everywhere in async operations:

const fs = require('fs');

function readFileAndProcess(filename) {
    let fileData = null;

    fs.readFile(filename, 'utf8', (err, data) => {
        if (err) {
            console.error('Error reading file');
            return;
        }

        fileData = data;
        console.log('File read complete');
        processData();
    });

    function processData() {
        if (fileData) {
            console.log('Processing:', fileData.toUpperCase());
        }
    }
}

Best Practices for Working with Closures

Let’s wrap up the technical part with some tips to keep your closure code clean and efficient.

Do’s

  1. Use closures for data privacy – They’re perfect for hiding implementation details
  2. Create specialized functions – Function factories can make your code more reusable
  3. Be mindful of memory – Don’t accidentally keep large objects alive longer than needed
  4. Name your inner functions – This helps with debugging and code readability
  5. Keep closures small – A closure should only capture what it really needs

Don’ts

  1. Avoid modifying outer variables unnecessarily – It can make code harder to understand
  2. Don’t create closures in loops without understanding the implications – We saw this earlier!
  3. Avoid excessive nesting – Too many nested closures can make code hard to follow
  4. Don’t rely on closures for performance-critical code – They have some overhead

Frequently Asked Questions

Q: Are closures only in JavaScript?

A: No! Many programming languages support closures, including Python, Ruby, Swift, and others. The concept is similar across languages.

Q: Do arrow functions create closures?

A: Yes, arrow functions also create closures. They behave similarly to regular functions but have different this binding.

function outer() {
    let x = 10;
    return () => console.log(x); // This arrow function creates a closure
}

Q: How can I check if a function is using a closure?

A: In browser DevTools, you can set a breakpoint and look at the “Scope” panel. It will show you the closure’s variables.

Q: Can closures access the outer function’s this and arguments?

A: Arrow functions don’t have their own this or arguments, so they’ll inherit from the outer scope. Regular functions have their own this and arguments, but they still close over variables.

Q: Are closures created every time a function is defined?

A: Yes, each time a function is created, it carries its lexical environment with it. But if multiple functions share the same outer scope, they’ll share the same closure variables.

Q: What’s the difference between a closure and a regular function?

A: A regular function only has access to its own parameters and global variables. A closure has access to its own parameters, global variables, AND variables from its outer function(s).


Putting It All Together

Let’s build something practical that uses everything we’ve learned. How about a simple task manager?

function createTaskManager() {
    // Private data
    let tasks = [];
    let nextId = 1;

    // Private helper function
    function findTaskIndex(id) {
        return tasks.findIndex(task => task.id === id);
    }

    // Public API (all methods use closures)
    return {
        addTask(title) {
            const task = {
                id: nextId++,
                title: title,
                completed: false,
                createdAt: new Date()
            };
            tasks.push(task);
            console.log(`Task added: "${title}"`);
            return task;
        },

        completeTask(id) {
            const index = findTaskIndex(id);
            if (index !== -1) {
                tasks[index].completed = true;
                console.log(`Task ${id} marked complete`);
                return true;
            }
            console.log(`Task ${id} not found`);
            return false;
        },

        removeTask(id) {
            const index = findTaskIndex(id);
            if (index !== -1) {
                const removed = tasks.splice(index, 1)[0];
                console.log(`Removed task: "${removed.title}"`);
                return true;
            }
            return false;
        },

        listTasks(showCompleted = true) {
            console.log('\n--- All Tasks ---');
            tasks.forEach(task => {
                const status = task.completed ? '✓' : '○';
                console.log(`${status} [${task.id}] ${task.title}`);
            });
            console.log('----------------\n');
        },

        getStats() {
            const total = tasks.length;
            const completed = tasks.filter(t => t.completed).length;
            const pending = total - completed;

            return {
                total,
                completed,
                pending,
                completionRate: total ? Math.round((completed / total) * 100) : 0
            };
        }
    };
}

// Let's use our task manager
const myTasks = createTaskManager();

myTasks.addTask('Learn about closures');
myTasks.addTask('Write an article about closures');
myTasks.addTask('Practice coding with closures');

myTasks.listTasks();

myTasks.completeTask(1);
myTasks.completeTask(3);

console.log('Stats:', myTasks.getStats());

myTasks.removeTask(2);

myTasks.listTasks();

See how all the task data is completely private? The only way to interact with it is through the methods we provided. That’s the power of closures!


Conclusion

Wow, we’ve covered a lot! From understanding what closures are to building real applications with them. Let’s recap the key points:

  • Closures are functions that remember the environment they were created in
  • They give us data privacy and encapsulation
  • We can use them for function factories, memoization, and modules
  • Watch out for common pitfalls like the loop variable problem
  • Closures are used everywhere in modern JavaScript, from React to Node.js

Remember, closures aren’t something to be afraid of. They’re just JavaScript’s way of being helpful, making sure functions remember what they need to do their job. The more you work with them, the more natural they’ll feel.

Image placeholder

Lorem ipsum amet elit morbi dolor tortor. Vivamus eget mollis nostra ullam corper. Pharetra torquent auctor metus felis nibh velit. Natoque tellus semper taciti nostra. Semper pharetra montes habitant congue integer magnis.

Leave a Comment