article

Understanding JavaScript Closures

2 min read

Closures are a fundamental concept in JavaScript that allow functions to access variables from their outer scope. This article explains closures, their practical uses, and common pitfalls.

What Are Closures?

A closure is the combination of a function bundled together with references to its surrounding state. In plain terms: the inner function remembers the variables of its parent, even after the parent has finished executing.

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

const closureExample = outerFunction("hello from outer");
closureExample("hello from inner");
// Outer Variable: hello from outer
// Inner Variable: hello from inner

A Practical Use Case: Counters

Closures are the natural way to encapsulate private state without a class.

function makeCounter() {
  let count = 0;
  return {
    increment() { count++; },
    decrement() { count--; },
    value()     { return count; },
  };
}

const counter = makeCounter();
counter.increment();
counter.increment();
console.log(counter.value()); // 2

count is completely private. There is no way to touch it from outside makeCounter.

Closures in the Wild

You encounter closures every time you write a callback, event handler, or factory function:

// Event handler captures `buttonId` from the loop variable
buttons.forEach((btn, buttonId) => {
  btn.addEventListener("click", () => {
    console.log(`Button ${buttonId} clicked`);
  });
});

Without closures, every handler would print the same final value of buttonId.

Common Pitfall: The Loop Problem

The classic trap — using var inside a loop creates one shared binding:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3

Fix it with let (which creates a new binding per iteration) or wrap in an IIFE:

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2

Memory Considerations

Closures keep their outer scope alive as long as the inner function is reachable. This is usually fine, but watch out for unintentionally long-lived references — for example, storing a closure in a global variable that holds a reference to a large object graph.

Takeaway

Closures are not a trick or an edge case — they are how JavaScript lexical scoping works. Once that clicks, patterns like module encapsulation, currying, and memoization stop feeling magical and start feeling obvious.