Safeguard
Security

What Causes a Memory Leak in JavaScript (and How to Find One)

A memory leak in JavaScript happens when objects you no longer need stay reachable, so the garbage collector can never free them. Here is how they start and how to hunt them down.

Priya Mehta
DevSecOps Engineer
6 min read

A memory leak in JavaScript occurs when memory that your program no longer needs stays reachable, so the garbage collector refuses to reclaim it and heap usage climbs until the tab or process falls over. JavaScript is garbage-collected, which fools a lot of developers into thinking leaks are impossible. They are not. The engine only frees objects it can prove are unreachable, and it is surprisingly easy to keep a reference alive by accident.

Understanding what a memory leak in JavaScript really is starts with how the engine decides what to keep.

How garbage collection actually works

V8 and other modern engines use a reachability model. Starting from a set of roots (the global object, the current call stack, active closures), the collector walks every reference it can follow. Anything it reaches is considered live and stays in memory. Anything it cannot reach is garbage and gets swept.

The consequence is simple but easy to forget: an object is not freed because you "stopped using" it. It is freed because nothing points to it anymore. A javascript memory leak is almost always a reference you forgot you were still holding.

const cache = {};

function remember(key, value) {
  cache[key] = value; // grows forever, nothing ever deletes
}

That cache is rooted by the module scope. Every value you put in stays live for the lifetime of the process. It is not a bug in the language; it is a design that never releases.

The four classic causes

If you ask what is a memory leak in JavaScript in practice, most cases fall into a handful of patterns.

Accidental globals. Assigning to an undeclared variable in non-strict mode attaches it to the global object, which is a root. It never gets collected. Turn on strict mode ('use strict') and this whole category disappears.

Forgotten timers and intervals. A setInterval that is never cleared keeps its callback, and everything the callback closes over, alive indefinitely.

function startPolling(widget) {
  setInterval(() => widget.refresh(), 1000);
  // widget can never be collected while the interval lives
}

Detached DOM nodes. You remove a node from the document but keep a reference to it in a JavaScript variable or array. The node, its children, and any listeners attached to them stay in the heap.

Closures that capture too much. A closure keeps its entire enclosing scope alive. If a long-lived callback closes over a large object it only needed briefly, that object stays resident.

Event listeners: the most common offender

In real applications, the memory leak javascript developers hit most often comes from event listeners that are added but never removed. Every addEventListener creates a strong reference from the event target to your handler. If the target outlives the component that registered the handler, the handler and its closure leak.

class Chart {
  constructor() {
    this.onResize = () => this.redraw();
    window.addEventListener('resize', this.onResize);
  }
  destroy() {
    window.removeEventListener('resize', this.onResize); // essential
  }
}

The rule of thumb: for every listener you add, know exactly where you remove it. Frameworks make this cleaner. React's useEffect cleanup, Vue's onUnmounted, and Angular's ngOnDestroy exist largely so you have a deterministic place to tear listeners down.

Finding a leak with Chrome DevTools

Suspecting a leak is one thing; proving it is another. The workflow that consistently works:

  1. Open the Memory tab in Chrome DevTools and take a heap snapshot.
  2. Perform the action you suspect leaks (open and close a modal, navigate away and back) several times.
  3. Take a second snapshot and use the Comparison view.
  4. Look for object counts that grow with each cycle and never drop.

The Performance panel's memory timeline is the fast first check. Record while you repeat an action; a healthy app shows a sawtooth (allocate, collect, allocate) while a leak shows a staircase that only climbs. In Node.js, run with --inspect and connect the same DevTools, or capture heap snapshots programmatically with the v8 module and diff them offline.

Leaks are also a security concern

A steadily growing heap is not only a performance problem. On a server, unbounded memory growth is a denial-of-service vector: an attacker who can trigger the leaking path repeatedly can exhaust memory and crash the process. Caches without eviction, per-request objects retained in a module-level array, and unbounded in-memory session stores are the usual suspects. Treat "memory grows under load and never recovers" as a reliability and availability finding, not a cosmetic one.

This matters in dependencies too. A leak in a third-party package runs in your process with your privileges. Reviewing the runtime behavior of what you pull in is part of the job, and an SCA tool such as Safeguard can flag packages with known resource-exhaustion advisories before they ship. If you are new to that discipline, our Academy has a short primer on triaging dependency risk.

Preventing leaks before they start

  • Prefer WeakMap and WeakSet for metadata keyed by objects you do not own; their entries do not prevent collection.
  • Give every cache a bound (size limit or TTL). An unbounded cache is a leak with good intentions.
  • Clear timers, abort fetches with AbortController, and remove listeners in teardown code.
  • Avoid storing DOM references longer than the nodes live.
  • Null out large objects you are done with if they sit in a long-lived scope.

None of these are exotic. Most leaks come from ordinary code that simply forgot to let go.

FAQ

Can JavaScript have memory leaks if it has garbage collection?

Yes. Garbage collection only frees objects that are unreachable. If your code keeps a reference alive (in a global, a cache, a timer, or a closure), the collector correctly leaves it in memory. The leak is the reference you forgot, not a flaw in the collector.

How do I know if my app has a memory leak?

Repeat a single action many times and watch the heap. In the browser, use the DevTools Performance memory timeline; a leak shows a staircase that never falls back. Confirm with two heap snapshots and the comparison view, looking for object counts that grow every cycle.

What is the difference between high memory usage and a memory leak?

High memory usage stabilizes: the app uses a lot but releases it and levels off. A leak never levels off. Memory retained by a leak grows without bound as you repeat the leaking operation, eventually forcing a crash or a browser tab reload.

Are event listeners really the main cause?

In UI-heavy applications, yes. Listeners create strong references from long-lived targets like window or document to short-lived component code. Always pair each addEventListener with a matching removeEventListener in your teardown path.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.