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 chanceLarge 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()inonModuleDestroy().
- Prisma:
- Streams: always
.destroy()/.end()on errors and timeouts. - EventEmitter:
emitter.removeListener/removeAllListenerson unload. - Avoid unbounded
setInterval; usesetTimeout+ 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/clinicfor 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
setIntervalwithout an explicit clear.
Short Answer
Interview readyPremium
A concise answer to help you respond confidently on this topic during an interview.