Toolmingo
Guides7 min read

JavaScript Closures: Complete Guide with Examples

Master JavaScript closures with clear explanations and real-world examples. Covers lexical scope, closure use cases (counters, memoization, private state, event handlers), common pitfalls, and interview questions.

Closures are one of JavaScript's most powerful (and most misunderstood) features. Once you truly understand them, you unlock private state, memoisation, factory functions, and much of what makes modern JavaScript tick — including how React hooks work under the hood.


Quick-reference table

Concept What it means
Lexical scope A function can see variables from the scope where it was defined, not where it's called
Closure A function that retains access to its outer scope after the outer function has returned
Closed-over variable A variable from an outer scope that a closure keeps alive
IIFE Immediately-invoked function expression — classic way to create a private scope
Module pattern Using a closure to expose a public API while hiding private state

What is a closure?

A closure is created every time a function is defined inside another function and that inner function references variables from the outer function's scope.

function makeGreeting(name) {
  // name is defined in the outer scope
  return function greet() {
    // greet "closes over" name — it remembers it even after makeGreeting returns
    console.log(`Hello, ${name}!`);
  };
}

const greetAlice = makeGreeting("Alice");
const greetBob   = makeGreeting("Bob");

greetAlice(); // Hello, Alice!
greetBob();   // Hello, Bob!

After makeGreeting("Alice") returns, the local variable name would normally be garbage-collected — but because greet still references it, the JavaScript engine keeps it alive. That surviving reference is the closure.


Lexical scope: the foundation

Closures are a direct consequence of lexical scope — the rule that a function's scope is determined by where in the source code it is written, not where it is called.

const value = "global";

function outer() {
  const value = "outer";

  function inner() {
    console.log(value); // "outer" — sees the nearest enclosing scope
  }

  inner();
}

outer();

The lookup order (scope chain) goes: local → outer function → … → global. Closures preserve this chain.


Real-world use cases

1. Private counter (encapsulation)

function makeCounter(start = 0) {
  let count = start; // private — nothing outside can touch it directly

  return {
    increment() { count += 1; },
    decrement() { count -= 1; },
    value()     { return count; },
    reset()     { count = start; },
  };
}

const counter = makeCounter(10);
counter.increment();
counter.increment();
console.log(counter.value()); // 12
console.log(counter.count);   // undefined — truly private

2. Memoization (caching expensive results)

function memoize(fn) {
  const cache = new Map(); // closed over by the returned function

  return function (...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key); // cache hit
    }
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

const expensiveSqrt = memoize(Math.sqrt);
expensiveSqrt(144); // computed
expensiveSqrt(144); // returned from cache

3. Partial application (function factories)

function multiply(a) {
  return function (b) {
    return a * b; // a is closed over
  };
}

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

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

4. Event listeners with private data

function setupButton(buttonId, message) {
  const btn = document.getElementById(buttonId);

  btn.addEventListener("click", function () {
    // message is closed over — no need to store it elsewhere
    alert(message);
  });
}

setupButton("btn-ok",     "Saved successfully!");
setupButton("btn-cancel", "Operation cancelled.");

5. Module pattern (pre-ES6)

const cart = (function () {
  const items = []; // private

  return {
    add(item)    { items.push(item); },
    remove(item) {
      const i = items.indexOf(item);
      if (i !== -1) items.splice(i, 1);
    },
    total()      { return items.reduce((sum, i) => sum + i.price, 0); },
    list()       { return [...items]; }, // return a copy, not the original
  };
})();

cart.add({ name: "Book", price: 15 });
cart.add({ name: "Pen",  price: 2  });
console.log(cart.total()); // 17

The classic var loop bug (and the fix)

This is the most common closure gotcha in interviews:

// BUG — all callbacks share the same `i`
for (var i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i); // prints 3, 3, 3
  }, 100);
}

Why? var is function-scoped, so all three callbacks close over the same i. By the time the callbacks fire, the loop has finished and i === 3.

Fix 1 — use let (block-scoped, a new binding per iteration):

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

Fix 2 — IIFE to capture the value (pre-ES6 technique):

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

Closures in React hooks

React hooks rely heavily on closures. Each render captures the current value of state and props:

function Counter() {
  const [count, setCount] = React.useState(0);

  // This handler closes over the current `count` at the time of render
  const handleClick = () => {
    setCount(count + 1);
  };

  return <button onClick={handleClick}>{count}</button>;
}

The "stale closure" problem occurs when a closure captures an outdated value:

function Timer() {
  const [count, setCount] = React.useState(0);

  React.useEffect(() => {
    const id = setInterval(() => {
      // BUG: `count` is always 0 — captured at mount time
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []); // empty deps = runs once
}

Fix: use the functional form of the updater, which receives the current value:

setCount(prev => prev + 1); // no closure over `count` needed

Closure vs plain function: memory implications

A closure keeps its outer scope alive as long as the inner function exists. This is usually fine, but can cause memory leaks if closures outlive their usefulness:

function attachHandler(element) {
  const hugeArray = new Array(1_000_000).fill("data"); // large allocation

  element.addEventListener("click", function () {
    console.log(hugeArray.length); // closure keeps hugeArray alive
  });

  // Even after attachHandler returns, hugeArray is NOT garbage-collected
  // because the event listener still holds a reference to it.
}

Fix: reference only what you need, or remove the listener when it's no longer needed.

element.removeEventListener("click", handler);

6 common mistakes

Mistake Problem Fix
var in a loop All iterations share one binding Use let or an IIFE
Stale closure in useEffect Hook captures initial value only Use functional updater or add deps
Large objects in closures Memory retained longer than needed Close over only the values you need
Mutating closed-over variables unexpectedly Shared mutable state causes bugs Treat closed-over state as private; never expose raw references
Forgetting return Inner function exists but is never surfaced Double-check the function returns the closure
Assuming closures copy values Primitives are copied by value; objects are shared by reference Use {...obj} or [...arr] to snapshot if needed

6 FAQ

Q: Is every function in JavaScript a closure?
Technically yes — every function holds a reference to its outer scope chain. In practice, we call a function a "closure" when it actively uses variables from an outer scope that has already returned.

Q: What's the difference between a closure and a callback?
A callback is a function passed to another function to be called later. A closure is a function that captures its surrounding scope. A callback is often also a closure, but the terms describe different properties.

Q: Do closures cause performance issues?
Rarely. Modern JS engines optimize closures aggressively. The main concern is unintentional memory retention (see the hugeArray example above).

Q: How are closures different from classes?
Both can encapsulate private state. Closures use function scope; classes use #privateField syntax. Closures are lighter and don't require this. Classes are better when you need inheritance or many instances with shared methods on a prototype.

Q: Can closures see variables declared after them?
No. Closures capture the scope at definition time. Variables declared after the closure (but in the same scope) are hoisted to the function scope but initialised to undefined until assignment. Accessing them before assignment gives undefined (or a ReferenceError for let/const — the "temporal dead zone").

Q: What does "the closure closes over" mean?
It's shorthand for "the inner function retains a live reference to the variable in its outer scope". The variable is said to be "closed over" — enclosed inside the closure's scope chain.


Closures show up everywhere in JavaScript: event handlers, timers, React hooks, the module pattern, currying, and memoization. Nail this concept and you'll both write better code and ace the JavaScript interview questions that trip up most developers.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools