loading bytebasex.com
Latest

JavaScript Closures Explained

Learn what JavaScript closures are, how they preserve variable references, how to fix the classic loop bug with let, and practical uses..

Try this in your browser console:

function makeCounter() {
  let count = 0;
  return function () {
    count++;
    return count;
  };
}

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

The makeCounter function already finished running after the first line. Its local variable, count, should be gone, cleaned up, out of scope. And yet every time you call counter(), it remembers exactly where it left off. This isn't a special trick or a library feature. It's a closure, and it's one of the most fundamental things JavaScript does, quietly, in almost every piece of code you write.

JavaScript Closures Explained

Welcome to BytebaseX. I'm Suptojit Modak, and in this guide, I'll break down what a closure actually is, the classic loop bug that closures explain, and where you've probably already been using closures without realizing it, including in our own guide on debouncing and throttling.

What a Closure Actually Is

A closure is what happens when a function "remembers" the variables from the place it was created, even after that outer function has already finished running. In the example above, the inner function returned by makeCounter keeps a live reference to count, not a copy of whatever value it had at the time.

A closure isn't a snapshot of a variable's value. It's a live reference. If the remembered variable changes later, the closure sees the updated value, not the old one.

Every function in JavaScript forms a closure over the scope it was defined in, whether or not you ever use that ability. Most of the time this is invisible, since most functions don't outlive their surrounding scope in a way that matters. The counter example makes it visible specifically because the inner function got returned and kept alive after the outer one finished.

A Live Demo: Two Independent Counters

Each call to makeCounter() creates a brand new, completely separate count variable. Click each button below to see two counters tracking their own state independently, with no shared variable anywhere.

0

0

Click each button a different number of times. Neither counter affects the other, because each one closes over its own separate count variable.

The Classic Bug Closures Explain: Loops and var

This is probably the single most common way closures actually show up as a real bug in real code:

for (var i = 1; i <= 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Logs: 4, 4, 4  (not 1, 2, 3)

This surprises almost everyone the first time. Here's why it happens: var is not scoped to each loop iteration, it's scoped to the entire function (or the global scope). There's only ever one i variable, shared by all three setTimeout callbacks. By the time any of them actually run, 100 milliseconds later, the loop has already finished and i has already reached 4.

The fix is almost embarrassingly simple once you know what's happening:

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

Unlike var, let creates a brand new binding of i for every single loop iteration. Each setTimeout callback closes over its own separate i, frozen at the value it had during that specific pass through the loop. This one-word swap fixes an entire category of bugs, and it's exactly why let is generally preferred over var in modern JavaScript.

A Practical Use: Private Variables

JavaScript doesn't have traditional private class fields in every environment, but closures have been used to fake genuinely private state for years:

function createBankAccount(startingBalance) {
  let balance = startingBalance;

  return {
    deposit(amount) { balance += amount; return balance; },
    withdraw(amount) {
      if (amount > balance) throw new Error("Insufficient funds");
      balance -= amount;
      return balance;
    },
    getBalance() { return balance; }
  };
}

const account = createBankAccount(100);
account.deposit(50);
console.log(account.getBalance()); // 150
console.log(account.balance); // undefined, not accessible directly

There is no way to reach into account and directly change balance from outside. The only way to affect it is through the methods that were specifically given access, because those methods are the only things that closed over that variable when it was created. This pattern, sometimes called the module pattern, was the standard way to get real encapsulation in JavaScript for years before native private class fields existed.

Where You're Already Using Closures

If you've written a debounce function, you've written a closure, whether you thought of it that way or not:

function debounce(func, delay) {
  let timeoutId; // <-- this variable is closed over
  return function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func.apply(this, args), delay);
  };
}

The timeoutId variable has to persist between calls to the returned function, remembering the previous timer so it can be cancelled. That's only possible because the returned function forms a closure over timeoutId. This is covered from the debounce side, without dwelling on the closure mechanics, in our guide on debouncing and throttling in JavaScript. Event handlers, callback functions, and anything returned from a factory function almost always rely on this same mechanism.

A Real Memory Consideration

Closures keep their referenced variables alive in memory for as long as the closure itself exists, since JavaScript can't garbage-collect a variable that something still might use. This is rarely a real problem, but it's worth knowing about in one specific case: if a closure holds onto a reference to a large object (a big array, a DOM element) that's no longer actually needed, that object stays in memory longer than it should, simply because something still has a closure pointing at it.

Warning: This becomes a genuine memory leak risk mainly with long-lived closures, like event listeners that are never removed, each holding a reference to something large. For short-lived closures, like a debounce function or a one-off callback, this is not something to worry about in practice.

Quick Reference

ConceptWhat it means
ClosureA function that remembers variables from where it was created, even after that outer scope has finished running
var in a loopShared across all iterations, causing the "same final value every time" bug with async callbacks
let in a loopCreates a fresh binding per iteration, each closure gets its own correct value
Module patternUsing a closure to create variables that can only be accessed through specific returned functions

Frequently Asked Questions

Do closures only happen with returned functions?

No, returning a function is just the clearest way to demonstrate one. Any function passed elsewhere and called later, like an event listener, a setTimeout callback, or a function passed into .map(), also forms a closure over its surrounding scope. Returning it is simply the easiest case to reason about.

Does every function in JavaScript create a closure?

Technically yes, every function closes over its surrounding scope by default. It only becomes noticeable, and gets called "a closure" in casual conversation, when a function is used somewhere that actually relies on remembering an outer variable after the original scope would otherwise be gone.

Can multiple closures share the same variable?

Yes, if they're created within the same scope at the same time. This is exactly what causes the classic var-in-a-loop bug: every callback closes over the exact same shared variable, rather than each getting an independent copy.

Are closures unique to JavaScript?

No, closures exist in many languages that treat functions as first-class values, including Python, Ruby, Swift, and Go. The concept is the same everywhere: a function retaining access to variables from its defining scope. JavaScript just happens to be where most web developers first run into the term.

Why does this matter if I never write factory functions like createBankAccount?

Because closures show up constantly in ordinary code you probably already write: event handlers referencing outer variables, callbacks passed to array methods, and any debounce or throttle implementation. Understanding closures explains behavior you'll run into regularly, even if you never deliberately set out to "use a closure."

Conclusion

A closure is just a function plus the variables it remembers from where it was born. Once that clicks, a whole category of "why does this variable have the wrong value" bugs, especially the classic loop-and-setTimeout case, stops being mysterious. And the next time you write a debounce function, a counter, or an event handler that references an outer variable, you'll recognize exactly what's making it work under the hood.

About the author

Suptojit Modak
Suptojit Modak
I'm Suptojit Modak, a web developer and the person behind BytebaseX — a blog with tutorials, guides, and free resources for developers and bloggers, built to be simple and easy to follow.

Instagram · GitHub · Facebook

Post a Comment