PureDevTools

JavaScript WeakRef & FinalizationRegistry Guide

Weak references and cleanup callbacks — patterns, pitfalls, and code template generator for WeakCache, DOM trackers, and resource managers

All processing happens in your browser. No data is sent to any server.

16 patterns

|

You’re building an image editor that caches loaded images for undo/redo. A Map of 50 full-resolution images eats 800MB and the tab crashes. You need cached entries to be garbage-collected when memory pressure hits — but you also need a cleanup callback to revoke the object URLs. WeakRef gives you the weak cache; FinalizationRegistry gives you the cleanup hook. The two APIs are always used together, and getting the interaction wrong causes either memory leaks or use-after-free bugs.

Why This Guide (Not MDN)

MDN documents WeakRef and FinalizationRegistry on separate pages with minimal examples. This guide covers both APIs together — the caching pattern, the cleanup pattern, the pitfalls (non-deterministic GC, revival hazards, deref() timing), and the real-world use cases where weak references actually make sense. Everything runs in your browser; no data is sent anywhere.

JavaScript WeakRef & FinalizationRegistry

JavaScript’s WeakRef and FinalizationRegistry — introduced in ES2021 (ES12) — give developers fine-grained control over weak references and garbage collection (GC) callbacks. Together they enable advanced patterns like self-evicting caches, DOM element trackers, and resource leak detection that are otherwise impossible or memory-leaky in plain JavaScript.

What Is a Weak Reference?

A strong reference is a normal JavaScript variable assignment. While a strong reference exists, the garbage collector cannot reclaim the target object.

A weak reference (via WeakRef) holds a pointer to an object without counting as a strong reference. If no other strong references exist, the GC is free to reclaim the object. The WeakRef itself remains valid — but deref() will return undefined after collection.

const obj = { data: new ArrayBuffer(1024 * 1024) }; // ~1 MB

// Strong reference — prevents GC:
const strong = obj;

// Weak reference — does NOT prevent GC:
const weak = new WeakRef(obj);

// If we release the strong reference:
// strong = null; obj = null;
// The GC may now collect the ArrayBuffer.
// weak.deref() → undefined (after collection)

WeakRef API

Constructor

new WeakRef(target)

target must be an object (including functions, arrays, DOM elements) or a registered Symbol (Symbol.for(...)). Passing a primitive (number, string, boolean, null, undefined) or a local Symbol throws TypeError.

deref()

const obj = ref.deref(); // object or undefined

Returns the referenced target if still alive, or undefined if it has been garbage collected. Always store the result in a local variable and check before use.

Same-Turn Guarantee

Within a single synchronous job (one turn of the event loop), if deref() returns non-undefined once, it will continue to do so for the duration of that synchronous block. Across await boundaries or setTimeout callbacks, always re-check.

FinalizationRegistry API

Constructor

const registry = new FinalizationRegistry(cleanupCallback);

cleanupCallback is a function that receives the held value (not the target — it’s already gone). Called sometime after the registered target is garbage collected.

register(target, heldValue, unregisterToken?)

registry.register(obj, "metadata-key");
// or with an unregister token:
const token = {};
registry.register(obj, "metadata-key", token);

unregister(token)

registry.unregister(token); // returns true if entry was removed

Cancels the pending cleanup callback. Use this when you clean up explicitly and don’t want the callback to fire afterward.

Common Use Cases

1. Self-Evicting Cache

The most practical use case: a cache that releases entries automatically when values are no longer needed anywhere else in the application.

class WeakCache {
  #map = new Map();
  #registry = new FinalizationRegistry((key) => this.#map.delete(key));

  set(key, value) {
    const ref = new WeakRef(value);
    this.#map.set(key, ref);
    this.#registry.register(value, key, ref);
  }

  get(key) {
    return this.#map.get(key)?.deref();
  }
}

Unlike a WeakMap, this cache can use any key type (not just objects) and provides a get() method that returns undefined transparently when the value has been collected.

2. DOM Element Tracking

Track DOM elements and their associated state without preventing elements from being garbage collected after DOM removal.

const elements = new Map(); // id → WeakRef<Element>
const registry = new FinalizationRegistry((id) => elements.delete(id));

function track(id, element) {
  const ref = new WeakRef(element);
  elements.set(id, ref);
  registry.register(element, id, ref);
}

function getElement(id) {
  return elements.get(id)?.deref(); // undefined if GC'd
}

3. Resource Cleanup Safety Net

Use FinalizationRegistry to detect leaked resources — file handles, WebSocket connections, database cursors — that were not explicitly closed.

class Connection {
  static #registry = new FinalizationRegistry((id) => {
    console.warn(`Connection ${id} leaked! Forcing close.`);
    forceClose(id);
  });

  constructor(id) {
    this.id = id;
    Connection.#registry.register(this, id);
  }

  close() {
    Connection.#registry.unregister(this);
    // ... actual close logic
  }
}

4. Event Listener Auto-Cleanup

Hold handler objects weakly in event listeners to avoid memory leaks when handlers are destroyed.

function addWeakListener(target, type, handlerObj, method) {
  const ref = new WeakRef(handlerObj);
  const wrapper = (event) => {
    const obj = ref.deref();
    if (obj) obj[method](event);
    else target.removeEventListener(type, wrapper);
  };
  target.addEventListener(type, wrapper);
  return () => target.removeEventListener(type, wrapper);
}

Garbage Collection Behavior

Non-Determinism

The JavaScript specification intentionally makes GC timing non-deterministic:

This is by design — it prevents authors from relying on GC for program correctness.

GC Eligibility vs. Collection

An object becomes eligible for GC when no strong references remain. But eligibility does not mean immediate collection. The object may live for an unknown additional duration before the callback fires.

Strong reference dropped


Object becomes GC-eligible

    (some time passes — indeterminate)


GC collects object


FinalizationRegistry callback fires

    (some time may pass)


Your cleanup code runs

Common Pitfalls

PitfallProblemFix
Critical cleanup via FinalizationRegistryCallback may never runUse try/finally for deterministic cleanup
Missing deref() null checkTypeError on undefinedAlways const o = ref.deref(); if (!o) return;
Target in held valueStrong reference prevents GCOnly hold primitives/IDs as held value
WeakRef with primitivesTypeError at constructionOnly objects and registered Symbols allowed
Relying on callback orderingSpec gives no ordering guaranteeTreat each cleanup as independent

WeakRef vs WeakMap vs WeakSet

FeatureWeakRefWeakMapWeakSet
Holds value weakly✓ (values)
Key must be objectn/a
GC notificationvia FinalizationRegistry
Iterable
Can use primitive keysn/an/a
Access liveness checkvia deref()implicit.has()

Rule of thumb: prefer WeakMap for simple object→value associations (simpler, no registry needed). Use WeakRef when you need to hold and conditionally access a reference across time. Add FinalizationRegistry when you need a GC notification for cleanup.

Browser & Engine Support

EnvironmentWeakRefFinalizationRegistry
Chrome / Edge84+84+
Firefox79+79+
Safari14.1+14.1+
Node.js14.6+14.6+
Deno1.0+1.0+
TypeScript4.1+ (with target: ES2021)4.1+

Both APIs require target: "ES2021" or higher in tsconfig.json, or explicit lib entries.

Frequently Asked Questions

Does deref() return the same object every time?

Yes, as long as the target is alive. Within a single synchronous job, if deref() returns non-undefined, subsequent calls return the same object. Across async boundaries, the result may differ (the object may have been collected during the async gap).

Can FinalizationRegistry replace try/finally?

No. FinalizationRegistry callbacks are non-deterministic and may not run at all. Always use try/finally, using declarations (TC39 explicit resource management), or explicit close()/dispose() for deterministic cleanup. Use FinalizationRegistry only as a supplementary safety net.

Why can’t I iterate over WeakRef targets?

By design — if WeakRef provided iteration, engines would need to pin all referenced objects in memory during iteration, defeating the purpose. For enumerable weak collections, combine a regular Map (for keys) with WeakRef (for values) and FinalizationRegistry (for cleanup), as in the WeakCache pattern.

Is WeakRef safe to use in production?

Yes, with proper null-checking. The API is stable (ES2021), well-supported across modern engines, and correct TypeScript typings are available. The main gotcha is always checking deref() return values — failing to do so causes TypeErrors on collected objects.

What happens if I call register() multiple times for the same target?

Each call creates an independent entry. The cleanup callback fires separately for each registration. If you register the same target twice, the callback fires twice. Use unregister() with distinct tokens to manage multiple registrations.


All processing is done entirely in your browser — no data is sent to any server.

Related Tools

More JavaScript Tools