Closures are one of those interview topics that make people nervous. I've seen senior developers stumble trying to explain them, even though they use closures every day without thinking about it.

Here's the thing: closures aren't complicated. The concept is actually simple. The confusing part is usually the explanation, not the thing itself.

The One-Sentence Explanation

A closure is a function that remembers variables from where it was created, even after that outer function has finished running.

That's it. Let me show you what that means.

function createCounter() {
    let count = 0;  // This variable is "closed over"
    
    return function() {
        count++;
        return count;
    };
}

const counter = createCounter();

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

createCounter() runs and finishes. Normally, count would be garbage collected. But the inner function still has access to it. The inner function "closes over" the count variable—hence "closure."

It's like the function carries a backpack containing variables from its birthplace.

Why This Is Useful: Data Privacy

JavaScript doesn't have private variables like other languages. Closures give us a way to fake them:

function createBankAccount(initialBalance) {
    let balance = initialBalance;  // Private!
    
    return {
        deposit(amount) {
            if (amount > 0) {
                balance += amount;
            }
        },
        withdraw(amount) {
            if (amount > 0 && amount <= balance) {
                balance -= amount;
                return amount;
            }
            return 0;
        },
        getBalance() {
            return balance;
        }
    };
}

const account = createBankAccount(100);

account.deposit(50);
console.log(account.getBalance()); // 150

account.withdraw(30);
console.log(account.getBalance()); // 120

// Can't access balance directly
console.log(account.balance); // undefined

The balance variable is completely hidden. The only way to interact with it is through the methods we exposed. This is the Module Pattern, and it's built entirely on closures.

Function Factories

Another common use: creating specialized functions on the fly.

function createMultiplier(factor) {
    return function(number) {
        return number * factor;
    };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);
const tenX = createMultiplier(10);

console.log(double(5));  // 10
console.log(triple(5));  // 15
console.log(tenX(5));    // 50

Each function "remembers" its own factor. You've created three separate closures, each with its own captured value.

This pattern shows up everywhere: event handlers, React hooks, partial application, middleware...

The Classic Interview Trap

This question has tripped up thousands of developers:

for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

// What gets logged?

Most people expect: 0, 1, 2

Actual output: 3, 3, 3

Why? By the time the timeouts fire, the loop has finished. All three functions share the same i, which is now 3.

The fix with closures:

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

The IIFE creates a new scope for each iteration, capturing the current value of i in j.

Or just use let, which creates a new binding per iteration:

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

Closures in the Wild

You're already using closures. Here are some examples you might not have recognized:

// Event handlers
button.addEventListener('click', function() {
    // This function closes over `button` and any other outer variables
});

// React hooks
function Counter() {
    const [count, setCount] = useState(0);
    
    const increment = () => {
        setCount(count + 1);  // Closes over `count` and `setCount`
    };
    
    return ;
}

// Array methods
const numbers = [1, 2, 3];
const multiplier = 10;

const result = numbers.map(n => n * multiplier);  // Closes over `multiplier`

Memory Considerations

One thing to be aware of: closures keep their outer variables alive. Usually fine, but in loops or long-running processes, you can accidentally hold onto memory you don't need.

function setupHandler() {
    const hugeData = loadMassiveDataset();  // 100MB
    
    return function handleClick() {
        console.log('clicked');
        // hugeData is still in memory even though we don't use it!
    };
}

// Better: don't close over what you don't need
function setupHandler() {
    const hugeData = loadMassiveDataset();
    const summary = summarize(hugeData);  // Extract what you need
    
    return function handleClick() {
        console.log(summary);
        // hugeData can now be garbage collected
    };
}

Mental Model

When you're confused about closures, ask yourself:

  1. Where was this function created?
  2. What variables existed in that scope?
  3. The function can still access all of them.

That's really all there is to it. The function remembers its birthplace. Once you see it that way, closures stop being mysterious.

← Back to JavaScript Articles

Back to Home