DEV Community

JSGuruJobs
JSGuruJobs

Posted on

6 JavaScript Closure Patterns That Fix Stale State and Memory Leaks in Production

Closures are not theory. They are the reason your React state goes stale and your Node process leaks memory. Here are 6 patterns that fix real bugs using closures correctly.


1. Counter State Without Global Variables

The simplest closure use case still shows up in interviews and real code.

Before

let count = 0;

function increment() {
  count++;
  return count;
}
Enter fullscreen mode Exit fullscreen mode

After

function createCounter() {
  let count = 0;

  return function increment() {
    count++;
    return count;
  };
}

const counter = createCounter();
counter(); // 1
counter(); // 2
Enter fullscreen mode Exit fullscreen mode

The state is now private and isolated per instance. No accidental mutation from outside. This is the foundation of every hook and factory pattern.


2. Fixing the Classic Loop Closure Bug

This bug still appears in production code.

Before

for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 100);
}
// 3 3 3
Enter fullscreen mode Exit fullscreen mode

After

for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 100);
}
// 0 1 2
Enter fullscreen mode Exit fullscreen mode

var creates one shared binding. Every closure points to the same memory. let creates a new binding per iteration, so each closure captures a different value.


3. Debounce With Persistent State

Closures are how you keep state between function calls without globals.

Before

function debounce(fn, delay) {
  let timeout;

  return function (...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => fn(...args), delay);
  };
}
Enter fullscreen mode Exit fullscreen mode

This is already correct best practice. The closure stores timeout.

After (improved with context + perf)

function debounce(fn, delay) {
  let timeout;

  return function (...args) {
    const context = this;

    clearTimeout(timeout);
    timeout = setTimeout(() => {
      fn.apply(context, args);
    }, delay);
  };
}
Enter fullscreen mode Exit fullscreen mode

Now it preserves this and arguments correctly across calls. One closure replaces repeated timer allocations and reduces unnecessary executions.


4. Fixing React Stale Closure Bugs

This is where most developers fail interviews.

Before

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

  useEffect(() => {
    setInterval(() => {
      console.log(count);
    }, 1000);
  }, []);
}
Enter fullscreen mode Exit fullscreen mode

Logs 0 forever.

After

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

  useEffect(() => {
    const id = setInterval(() => {
      setCount(prev => {
        console.log(prev);
        return prev;
      });
    }, 1000);

    return () => clearInterval(id);
  }, []);
}
Enter fullscreen mode Exit fullscreen mode

The fix uses a functional update, not a captured variable. The closure now references the latest state instead of the initial render.

This exact class of bug is why closures dominate interviews .


5. Avoiding Hidden Memory Leaks in Closures

Closures capture entire environments, not individual variables.

Before

function createProcessor() {
  const hugeData = loadHugeData(); // 300MB
  const map = buildMap(hugeData);

  return function (id) {
    return map[id];
  };
}
Enter fullscreen mode Exit fullscreen mode

hugeData stays in memory forever, even if unused.

After

function createProcessor() {
  const map = (() => {
    const hugeData = loadHugeData();
    return buildMap(hugeData);
  })();

  return function (id) {
    return map[id];
  };
}
Enter fullscreen mode Exit fullscreen mode

Now only map is captured. Memory drops dramatically. If you deal with long running services, this pattern compounds with the Node.js memory leaks detection and resolution guide when analyzing heap snapshots.


6. Memoization With Closure Cache

Closures are the simplest way to build a cache.

Before

function slowSquare(n) {
  console.log("computing...");
  return n * n;
}
Enter fullscreen mode Exit fullscreen mode

After

function memoize(fn) {
  const cache = new Map();

  return function (arg) {
    if (cache.has(arg)) {
      return cache.get(arg);
    }

    const result = fn(arg);
    cache.set(arg, result);
    return result;
  };
}

const fastSquare = memoize(slowSquare);

fastSquare(4); // computing...
fastSquare(4); // cached
Enter fullscreen mode Exit fullscreen mode

The closure keeps cache alive across calls. This turns repeated O(n) work into O(1) lookups.


Closures are not a concept to memorize. They are a memory model. If you understand what gets captured and when, you can predict bugs before they ship.

Take one pattern here and apply it to your current codebase. The fastest way to level up is not learning new tools. It is fixing the bugs you already have.

Top comments (0)