JavaScriptIntermediate

JavaScript Closures Explained: What They Actually Are and Why They Matter

A closure isn't a special syntax feature — it's just a function that remembers the variables from where it was created. Here's what that actually means in practice, with the bugs it causes and the patterns it enables.

DevFieldGuideJuly 22, 2026 (updated July 26, 2026)6 min read
Share:

Closures show up in nearly every JavaScript codebase, often without anyone naming them explicitly — understanding the mechanism directly clears up a category of bugs that otherwise look mysterious.

The core idea

A closure is a function bundled together with references to the variables from the scope it was created in — even after that outer scope has finished running:

js
function makeCounter() {
  let count = 0;
  return function () {
    count += 1;
    return count;
  };
}
 
const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3

makeCounter() runs and returns, but the inner function still has access to count — it's not re-created or reset on each call, because the returned function closed over that specific count variable, not a copy of its value at creation time.

Each call creates a genuinely separate closure

js
const counterA = makeCounter();
const counterB = makeCounter();
counterA(); // 1
counterA(); // 2
counterB(); // 1 — its own separate `count`, unaffected by counterA

Every call to makeCounter() creates a fresh count variable and a fresh closure over it — counterA and counterB don't share state, even though they were created from the same function.

The classic loop bug closures explain

js
// The bug
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// logs: 3, 3, 3 — not 0, 1, 2

var is function-scoped, not block-scoped — there's only one i shared across all three loop iterations, and by the time the setTimeout callbacks actually run, the loop has finished and i is 3. Every closure captured the same variable, not a snapshot of its value at each iteration.

js
// The fix
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// logs: 0, 1, 2 — correct

let is block-scoped — each loop iteration gets its own fresh i, so each closure captures a genuinely distinct variable. This single distinction between var and let is the most common real-world encounter developers have with closures, usually while debugging exactly this symptom.

A practical use: private state without a class

js
function createBankAccount(initialBalance) {
  let balance = initialBalance;
 
  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);   // 150
account.withdraw(30);  // 120
// balance itself is not accessible from outside — only through the methods

balance is genuinely private — there's no way to read or modify it except through the returned methods, because it only exists inside the closure createBankAccount created. This "module pattern" predates JavaScript classes and is still a legitimate way to get real encapsulation without a class at all.

Closures and React

Anyone who's written a React component has used closures constantly, often without noticing:

jsx
function Counter() {
  const [count, setCount] = useState(0);
 
  function handleClick() {
    setCount(count + 1); // closes over `count` from this specific render
  }
 
  return <button onClick={handleClick}>{count}</button>;
}

handleClick closes over the count value from the render it was created in — this is exactly the mechanism behind React's "stale closure" bugs, where an event handler or effect callback references an outdated value because it captured an earlier render's variables, not the current one.

Closures and currying

Closures are also what makes currying — transforming a function that takes multiple arguments into a chain of functions each taking one — possible at all:

js
function multiply(a) {
  return function (b) {
    return a * b;
  };
}
 
const double = multiply(2);
double(5);  // 10
double(10); // 20
 
multiply(3)(4); // 12 — called directly, without an intermediate name

multiply(2) returns a closure that remembers a = 2 — every subsequent call to that returned function multiplies whatever's passed by the remembered a. This is the exact same mechanism as makeCounter above, applied to build a reusable, partially-configured function instead of stateful counters.

Debugging a closure with the browser's dev tools

When a closure holds a value you need to inspect, Chrome DevTools' scope inspector (visible when paused at a breakpoint inside the inner function) shows a "Closure" section listing exactly which outer variables the current function has captured — a genuinely useful way to confirm what a specific closure actually has access to, rather than reasoning about it purely from reading the source.

Closures in a memoization cache

A practical, common use of closures: caching expensive computation results without polluting the outer scope with a separate cache variable:

js
function memoize(fn) {
  const cache = new Map();
  return function (...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}
 
const slowSquare = (n) => { /* expensive work */ return n * n; };
const fastSquare = memoize(slowSquare);

cache is only accessible to the returned function — nothing outside memoize can read or corrupt it directly, which is exactly the same private-state pattern as the bank account example above, applied to a genuinely common real-world need.

Common mistakes

Common mistakes
  • Using var in a loop that creates closures (event handlers, timers, async callbacks) and being surprised every closure sees the same final value. Switch to let, or manually create a new scope per iteration with an IIFE if you're stuck with var for some reason.
  • Assuming a closure captures a variable's value at creation time. It captures a reference to the variable itself — if the outer variable changes later, every closure over it sees the new value, not a frozen snapshot.
  • Creating closures in a hot loop unnecessarily (a new function defined inside a frequently-called function, capturing the same unchanging outer values every time) — usually harmless, but worth hoisting the function out if it doesn't actually need to be recreated each call.
  • Not cleaning up closures that hold references to DOM nodes or large data structures in long-lived event listeners — the closure keeps everything it references alive, which is correct behavior but a real memory cost if the listener is never removed.

Frequently Asked Questions

DevFieldGuide
DevFieldGuide

Editorial Team

Practical tutorials and developer tools, written and maintained by the DevFieldGuide team.

Enjoyed this article?

Get the next one straight to your inbox, along with the best of what we publish each week.

Related Articles

More in JavaScript

View all