Skip to main content

How do you avoid memory leaks?

1) Basic principles

  • No reference means no leak. Anything reachable from the roots (global variables, closures, the DOM, caches) will not be garbage-collected.
  • Shorten objects' lifetimes and break connections in time (handlers, timers, caches, DOM references).

2) Typical sources of leaks and how to close them

Event handlers / subscriptions

Problem: dangling listeners keep the DOM/data alive.

javascript
const onClick = () => {}; button.addEventListener('click', onClick); // ... button.removeEventListener('click', onClick); // must be the same callback
  • In React: always do cleanup in useEffect.
javascript
useEffect(() => { const onScroll = () => {}; window.addEventListener('scroll', onScroll); return () => window.removeEventListener('scroll', onScroll); // }, []);
  • In Nest/EventEmitter: unsubscribe in onModuleDestroy().

Timers and intervals

javascript
const id = setInterval(tick, 1000); // ... clearInterval(id); //
  • Node 20+: use AbortSignal for timers:
javascript
const ac = new AbortController(); setTimeout(cb, 5000, { signal: ac.signal }); // ... ac.abort(); //

Unfinished fetch requests/streams

  • Cancel via AbortController, close streams.
javascript
const c = new AbortController(); const res = await fetch(url, { signal: c.signal }); // ... c.abort(); //

"Detached" DOM nodes

  • Don't keep references to nodes removed from the DOM.
  • Null out DOM caches after remove():
javascript
elem.remove(); elemRef = null; // give the GC a chance

Large closures

  • Don't close over large objects unnecessarily. Pass the minimum into callbacks.
  • Break references after use:
javascript
let big = getHugeObject(); doWork(() => { /* use only the small part */ }); big = null; //

Global singletons/caches

  • Anything in a module/global scope lives "forever". Limit its size, add cleanup, or use weak references.

3) Leak-free caches

  • Object keys → WeakMap/WeakSet (don't block the GC):
javascript
const memo = new WeakMap(); function compute(obj) { if (memo.has(obj)) return memo.get(obj); const val = heavy(obj); memo.set(obj, val); return val; }
  • For strings/numbers use an LRU with a limit:
javascript
class LRU { constructor(limit=500){ this.limit=limit; this.map=new Map(); } get(k){ const v=this.map.get(k); if(v){ this.map.delete(k); this.map.set(k,v); } return v; } set(k,v){ if(this.map.has(k)) this.map.delete(k); this.map.set(k,v); if(this.map.size>this.limit){ const oldest = this.map.keys().next().value; this.map.delete(oldest); // evict } } }

4) React / Next.js

  • Clean up all side effects (useEffect → return () => {...}).
  • Don't store huge objects in useRef/context unnecessarily.
  • Avoid endless microtask chains (chained Promises), they delay GC and rendering.
  • Unsubscribe from Observable/WebSocket in cleanup.
  • For large lists, use virtualization (fewer objects at once).

5) Node.js / NestJS

  • Close connections/pools:
    • Prisma: await prisma.$disconnect() on shutdown.
    • Redis/Mongo/pg: .quit()/.close()/.end() in onModuleDestroy().
  • Streams: always .destroy()/.end() on errors and timeouts.
  • EventEmitter: emitter.removeListener/removeAllListeners on unload.
  • Avoid unbounded setInterval; use setTimeout + a loop with checks/abort.
  • Handle signals:
javascript
process.on('SIGTERM', async () => { await app.close(); // close the server/DB/queues process.exit(0); });

6) Async work and queues

  • Set timeouts and concurrency limits (p-limit, semaphores).
  • For networking/streams, use backpressure: don't read faster than you process.
  • Don't keep large arrays of promises in memory; process them in chunks.

7) FinalizationRegistry - for soft cleanup only

  • Good for external caches/handles, but don't rely on the order/timing of the call.
  • Don't build business logic on finalizers.

8) Tools for finding leaks

Browser (Chrome)

  • Memory → Heap snapshot: look for "Detached HTML elements", retainers.
  • Allocation instrumentation: who creates and retains objects.
  • Performance: long microtasks/frequent GC are a sign of trouble.

Node

  • --inspect + Chrome DevTools → Heap snapshots.
  • heapdump/clinic for snapshots in production.
  • GC logs: node --trace-gc app.js (for diagnostics, not in prod).
  • Metrics: monitor RSS/heapUsed, alert on a growth trend.

9) Guardrails in code and CI

  • Limits on cache/queue sizes.
  • An E2E test: "warm it up N times → memory stabilizes".
  • Linter rules: forbid anonymous listeners where removal is needed, forbid setInterval without an explicit clear.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.