Let's get something out of the way first: closures aren't a special feature you opt into. They're a natural consequence of how JavaScript handles functions and scope. Once you understand that, the "closure" label starts to feel obvious rather than mysterious.

TL;DR

A closure is a function that remembers the variables from its outer scope even after that scope has finished executing. Every function in JavaScript that references variables from an enclosing scope is a closure.

1. First — What Is Scope?

Before closures make sense, scope has to make sense. Scope is the rule that determines which variables are accessible where. In JavaScript, each function creates its own scope — a private bubble of variables.

javascript — scope basics
function outer() {
  let message = "hello from outer";

  function inner() {
    console.log(message); // Can access outer's variable
  }

  inner(); // "hello from outer"
}

outer();
console.log(message); // ReferenceError — message doesn't exist out here

The inner function can see message because it's defined inside outer, where message lives. This is just how lexical scope works — functions have access to the variables of the scope they were written in, not where they're called from.

2. The Closure Moment

The interesting thing happens when a function outlives its parent scope. Normally when a function finishes executing, its variables are cleaned up. But if an inner function is returned or passed elsewhere, it carries those variables with it. That "carry" is the closure.

javascript — a closure in action
function makeCounter() {
  let count = 0;          // Lives inside makeCounter's scope

  return function() {     // This inner function is returned
    count++;              // It still has access to count
    return count;
  };
}

const counter = makeCounter();

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

After makeCounter() finishes running, its local scope should be gone. But it isn't — not entirely. The returned function holds a reference to count, keeping that variable alive in memory. That's the closure: the function plus the variables it captured.

Mental model

Think of a closure as a function with a backpack. When the function is created, it packs up all the variables it references from its surrounding scope. It carries that backpack wherever it goes.

3. Each Closure Gets Its Own Copy

This is where things get genuinely useful — and where a lot of bugs come from if you don't understand it. Every time you call makeCounter(), you get a brand new closure with its own independent count:

javascript — independent closures
const counterA = makeCounter();
const counterB = makeCounter();

console.log(counterA()); // 1
console.log(counterA()); // 2
console.log(counterB()); // 1  ← starts fresh, not 3
console.log(counterA()); // 3

counterA and counterB do not share their count. Each call to makeCounter creates a new scope, a new count, and a new closure. They're completely independent.

4. The Classic Loop Bug (and Why Closures Cause It)

This is probably the most-asked closure interview question. It trips people up because the output seems wrong — until you understand what's actually happening:

javascript — the loop closure trap
// Using var (the buggy version)
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}
// Output after 1s: 3, 3, 3   ← not 0, 1, 2 !

// Why? All three functions close over the SAME i.
// By the time they run, the loop has finished and i === 3.

All three callbacks share a single i because var is function-scoped, not block-scoped. The loop creates one variable, and all three closures reference it. By the time the timeouts fire, the loop is done and i is 3.

javascript — the fix with let
// Using let (the correct version)
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}
// Output after 1s: 0, 1, 2  ✓

// Why? let is block-scoped. Each iteration gets its OWN i.
// Each closure captures a different variable.

let creates a new binding for i on every loop iteration. So each callback closes over a different i — the one that existed at that particular moment in the loop. Problem solved.

5. Real Uses of Closures

Closures aren't just a concept to explain in interviews. They show up constantly in real JavaScript code. Here are a few patterns you've probably already used without realizing:

Data privacy (module-like pattern)

javascript — private state via closure
function createWallet(initialBalance) {
  let balance = initialBalance; // private — can't be accessed from outside

  return {
    deposit(amount) {
      balance += amount;
      console.log(`Deposited ${amount}. Balance: ${balance}`);
    },
    withdraw(amount) {
      if (amount > balance) {
        console.log("Insufficient funds");
        return;
      }
      balance -= amount;
      console.log(`Withdrew ${amount}. Balance: ${balance}`);
    },
    getBalance() {
      return balance;
    }
  };
}

const wallet = createWallet(100);
wallet.deposit(50);    // Deposited 50. Balance: 150
wallet.withdraw(30);   // Withdrew 30. Balance: 120
console.log(wallet.balance); // undefined — balance is private!

Function factories

javascript — creating specialized functions
function multiplier(factor) {
  return function(number) {
    return number * factor; // closes over factor
  };
}

const double = multiplier(2);
const triple = multiplier(3);

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

Event handlers (you've been using these all along)

javascript — closures in the DOM
function setupButton(buttonId, message) {
  const btn = document.getElementById(buttonId);

  btn.addEventListener('click', function() {
    console.log(message); // closes over message
  });
}

setupButton('btn1', 'Button 1 clicked!');
setupButton('btn2', 'Button 2 clicked!');

// Each button remembers its own message — that's closures.

6. Memory Considerations

Closures keep variables alive in memory. Usually that's exactly what you want. But in cases where you create many closures that hold onto large objects or DOM references, it can contribute to memory leaks if those closures hang around longer than they should.

This rarely matters in practice for normal application code, but it's worth knowing: a closure won't let its captured variables be garbage collected as long as the closure itself is reachable. If you attach closures to long-lived objects (like global variables or detached DOM nodes), those referenced variables stick around too.

Practical rule

Closures are not a performance problem to avoid — they're a feature to embrace. Just be mindful of closures that capture large datasets or DOM elements inside event listeners on elements you're dynamically adding and removing.

Summary

Closures are what happen when a function remembers the variables from the scope it was created in — even after that scope is gone. They're not a complex opt-in feature. They're how JavaScript functions naturally work.

The three things to hold onto: closures capture variables by reference (not by value), each closure gets its own independent copy of those variables, and let/const in loops create a new binding per iteration while var doesn't.

Once that clicks, you'll start seeing closures everywhere — in every callback, every event handler, every factory function. Not as a scary thing, just as the way functions in JavaScript carry their context with them.

A
Akash Das Dhibar
Web developer from West Bengal, India. Learning in public and writing about JS fundamentals.
GitHub LinkedIn
← Why === in JavaScript All posts →