Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Memory Management and DevTools  ›  Lesson

Debugging Memory Leaks

Debugging Memory Leaks 10 minMost real JavaScript memory leaks come from a small set of everyday patterns: an interval or event listener that's never removed, a closure that outlives its usefulness while holding a large structure, or a growing cache or array nothing ever trims — each one keeps objects reachable long after the program is done with them.

Debugging Memory Leaks

In short: a JavaScript memory leak almost always comes down to one thing: something is still reachable that shouldn't be — an interval still running, a listener still attached, a closure still holding onto data nobody needs anymore. None of these are exotic bugs; they're all ordinary code that simply never let go. This lesson is read-only — a real leak only shows up over time, in a real page, watched through DevTools' Memory tab, not in a single run of a snippet.

setInterval(() => { list.push(newItem) }, never clearInterval'd, overflowing into a bucket labelled list, growing, never trimmed; beside it, the fix: save the id from setInterval, and call clearInterval(id) once truly done — same idea for removeEventListener; a note that most real leaks are exactly this: something still running, still holding a reference, long after it should have stopped

An interval that outlives its purpose

The single most common leak: a setInterval (or setTimeout chain) that's never cleared.

function startPolling() {
  setInterval(() => {
    fetchLatestNotifications().then((list) => renderNotifications(list));
  }, 5000);
}
startPolling();

Nothing here ever calls clearInterval. Every five seconds, forever, this keeps running — and if startPolling gets called again later (say, the user navigates away and back), a second interval starts stacking on top of the first, each one still reachable through the timer system itself. The fix is always the same shape: keep the id, and clear it when you're genuinely done.

function startPolling() {
  const id = setInterval(() => {
    fetchLatestNotifications().then((list) => renderNotifications(list));
  }, 5000);
  return () => clearInterval(id); // call this when leaving the page
}

An event listener that's never removed

The same pattern, with addEventListener:

function setupSearchBox(input) {
  document.addEventListener("click", function handleOutsideClick(event) {
    if (!input.contains(event.target)) {
      input.value = "";
    }
  });
}

Every call to setupSearchBox registers another listener on document — a root that's always reachable — and none of them are ever detached. Each listener, plus anything it closes over, stays reachable forever through document. The fix is to keep a reference to the handler and call removeEventListener with it once the component using it goes away.

A cache that only ever grows

Not every leak involves timers or listeners — sometimes it's just a structure nothing ever trims:

const responseCache = {};

function cachedFetch(url) {
  if (responseCache[url]) return Promise.resolve(responseCache[url]);
  return fetch(url)
    .then((r) => r.json())
    .then((data) => {
      responseCache[url] = data; // added, but never removed
      return data;
    });
}

Every unique url ever fetched adds one more permanent entry to responseCache, which lives at module scope and is reachable for the life of the program. For a handful of URLs this is harmless; for a page that constructs many unique URLs over a long session, it grows without bound. A real fix caps the cache's size or expires old entries — the leak isn't the cache itself, it's a cache with no limit.

Reading the symptom in a heap snapshot

All three patterns share the same fingerprint in DevTools' Memory tab: take a snapshot, repeat the suspect action several times, take another snapshot, and compare. A genuine leak shows the count of some object type — listener functions, cache entries, detached DOM nodes — climbing between the two snapshots and never coming back down, even after the action that created them is long finished.

🎯 Your turn

A single-page app calls document.addEventListener("click", handleClick) every time a particular component is shown, but never calls removeEventListener when the component is hidden or removed. Explain why this leaks memory, and what visiting that part of the app repeatedly would do to a heap snapshot comparison.

Quick practice

quick drill

What do an uncleared setInterval and an unremoved event listener have in common as leak causes?

quick drill

What is the actual fix for an interval that's leaking memory?

quick drill

What pattern in two compared heap snapshots points to a real memory leak?

Practice — level up

0/1 solved

Write your answer, then compare it with a rubric and a model answer.

ExplainSpotting the leak

A single-page app calls document.addEventListener("click", handleClick) every time a particular component is shown, but never calls removeEventListener when the component is hidden or removed. Explain why this leaks memory, and what visiting that part of the app repeatedly would do to a heap snapshot comparison.