| |

JavaScript 59 🧬 WeakRef / FinalizationRegistry

let obj = { id: 1 };
const ref = new WeakRef(obj);

console.log(ref.deref());
console.log(ref.deref() === obj);

obj = null;

setTimeout(() => {
  console.log(ref.deref());
}, 1000);

const registry = new FinalizationRegistry((heldValue) => {
  console.log('Collected:', heldValue);
});

let target = { name: 'Alice' };
registry.register(target, 'target-object');
target = null;

setTimeout(() => {
  console.log('After GC check');
}, 5000);

class Resource {
  constructor(id) {
    this.id = id;
  }
  release() {
    console.log('Releasing', this.id);
  }
}

const finalizer = new FinalizationRegistry((id) => {
  console.log('Resource freed:', id);
});

let resource = new Resource('db-connection');
finalizer.register(resource, resource.id);
resource = null;

JavaScript is garbage-collected — you don’t manage memory manually. But sometimes you need to know when an object is collected, or hold a reference that doesn’t prevent collection. That’s what WeakRef and FinalizationRegistry provide. They’re advanced, rarely needed, and easy to misuse — but essential when the situation calls for them.

Key point: WeakRef lets you hold a reference to an object without keeping it alive. FinalizationRegistry lets you run a callback after an object is garbage-collected. Both are non-deterministic — the GC decides when, and it might never happen. Use them only when you’ve ruled out every other approach.


a – WeakRef

A WeakRef holds a weak reference to an object — a reference that doesn’t prevent garbage collection. When all strong references to the object are gone, the GC can reclaim it, and the WeakRef starts returning undefined.

Basic syntax:

const ref = new WeakRef(obj);
  • obj must be an object — WeakRef doesn’t work on primitives
  • ref.deref() returns the object, or undefined if it’s been collected

Getting the object back:

let obj = { id: 1 };
const ref = new WeakRef(obj);

console.log(ref.deref());
// [ { id: 1 } ]

console.log(ref.deref() === obj);
// [ true ]

After the strong reference is gone:

obj = null;
// GC eventually collects the object
// ref.deref() returns undefined

The GC runs when it wants — you can’t force it. So the object may still be alive for a while.

Why WeakRef exists:

Normally, holding a reference to an object keeps it alive — that’s what garbage collection protects against. But some caching and tracking patterns need to hold a hint about an object without preventing its cleanup. WeakRef is for those cases.

WeakRef vs WeakMap and WeakSet:

FeatureWeakRefWeakMapWeakSet
HoldsSingle objectObject → valueObject
Retrieval.deref().get(key).has(obj)
IterableN/A
Use caseCache entryMetadataTagging

A WeakRef is a single weak reference. WeakMap and WeakSet are collections of weak references to keys/values. WeakMap’s keys and WeakSet’s contents don’t prevent collection.

A cached object example:

class DataCache {
  constructor() {
    this.refs = new Map();
  }

  get(key) {
    const ref = this.refs.get(key);
    if (!ref) return null;
    const value = ref.deref();
    if (!value) {
      // GC collected it — remove stale entry
      this.refs.delete(key);
      return null;
    }
    return value;
  }

  set(key, value) {
    this.refs.set(key, new WeakRef(value));
  }
}

This holds cached objects without preventing them from being collected when memory is tight. The cache entries become stale and clean themselves up lazily.

The deref() method:

const ref = new WeakRef({ a: 1 });

const value = ref.deref();
if (value) {
  console.log(value.a);
  // [ 1 ]
} else {
  console.log('collected');
}

deref() returns either the object or undefined — always check.

WeakRef can’t be used on primitives:

new WeakRef(42);
// TypeError: Invalid value used in weak ref

new WeakRef('string');
// TypeError

new WeakRef({});
// ✅

Only objects — including arrays, functions, and other objects.

The GC is not immediate:

let obj = { id: 1 };
const ref = new WeakRef(obj);
obj = null;

console.log(ref.deref());
// [ { id: 1 } ]  ← still alive — GC hasn't run

Even though the strong reference is gone, the object may still be alive — the GC runs when it decides to.

When to use WeakRef:

Use caseWhy
Caching without blocking GCHold hint, not ownership
Tracking “recently used” itemsAvoid keeping alive forever
Avoiding memory leaksCleanup when target dies
Optional links between objectsWeak association

When NOT to use WeakRef:

SituationInstead
You need the objectUse a strong reference
You need deterministic cleanupUse try/finally
You need a proper cacheUse Map with eviction policy
You need to observe collectionUse FinalizationRegistry

WeakRef doesn’t guarantee collection:

The GC may never collect the object if memory pressure never increases. So you can’t rely on WeakRef becoming empty as a signal that something happened.

Common WeakRef patterns:

Pattern 1 — Weak cache with lazy cleanup:

const cache = new Map();

function getCached(key, factory) {
  const ref = cache.get(key);
  let value = ref?.deref();

  if (!value) {
    value = factory();
    cache.set(key, new WeakRef(value));
  }

  return value;
}

The cache doesn’t hold objects forever — they can be collected when no one else needs them.

Pattern 2 — Preventing memory leaks in event handlers:

class EventManager {
  constructor() {
    this.listeners = [];
  }

  add(target, handler) {
    const ref = new WeakRef(target);
    const wrapped = (event) => {
      const t = ref.deref();
      if (t) {
        handler.call(t, event);
      } else {
        // target was collected — remove listener
        this.remove(wrapped);
      }
    };
    target.addEventListener('click', wrapped);
    this.listeners.push(wrapped);
  }

  remove(wrapped) {
    // ...
  }
}

Pattern 3 — Secondary metadata:

class Tracker {
  constructor() {
    this.items = [];
  }

  track(obj) {
    this.items.push(new WeakRef(obj));
  }

  active() {
    return this.items
      .map(ref => ref.deref())
      .filter(Boolean);
  }
}

Pattern 4 — Symbol-keyed cache:

const weakCache = new WeakMap();

function getMeta(obj) {
  let meta = weakCache.get(obj);
  if (!meta) {
    meta = { created: Date.now() };
    weakCache.set(obj, meta);
  }
  return meta;
}

For metadata, WeakMap is often better than WeakRef — it associates data with an object without extending its lifetime.

When WeakRef is the right tool:

  • You want a cache that doesn’t prevent GC
  • You want a hint about an object that may or may not still exist
  • You want to track objects without owning them
  • You’re building infrastructure where holding strong references would leak

WeakRef and memory pressure:

let obj = { data: new Array(1e6).fill(0) };
const ref = new WeakRef(obj);
obj = null;

// Eventually, GC collects the big array
// ref.deref() → undefined

The GC runs when it needs memory. Under memory pressure, it collects more aggressively.


b – FinalizationRegistry

A FinalizationRegistry lets you run a callback when an object is garbage-collected. It’s the other half of the weak-reference toolkit — you use it to react to collection.

Basic syntax:

const registry = new FinalizationRegistry((heldValue) => {
  console.log('Object collected:', heldValue);
});

The callback receives a held value — a piece of data you provide at registration time.

Registering:

let obj = { name: 'Alice' };
registry.register(obj, 'my-held-value');

The second argument is passed to the callback when obj is collected.

After collection:

obj = null;
// Some time later, when GC runs:
// [ Object collected: my-held-value ]

Why FinalizationRegistry exists:

Sometimes you have side resources that aren’t tied to the JS heap — open files, network connections, memory outside the JS engine. When the JS object is collected, you want to clean up. FinalizationRegistry is the hook.

A resource cleanup example:

const finalizer = new FinalizationRegistry((id) => {
  console.log(`Freed resource ${id}`);
});

class Resource {
  constructor(id) {
    this.id = id;
    finalizer.register(this, id);
  }
  close() {
    console.log(`Closing ${this.id}`);
  }
}

{
  let r = new Resource('file-1');
  // ...
  // when `r` is collected, "Freed resource file-1" is logged
}

Warning: Do NOT rely on this for critical cleanup. The GC might never run, or it might run much later than you expect. Always provide an explicit close() method.

Held values:

The held value is not the object — it’s separate data:

registry.register(target, heldValue);

The held value is kept alive by the registry, even after target is collected. This is how you avoid referencing the target from its own cleanup callback.

const registry = new FinalizationRegistry((heldValue) => {
  console.log('Collected:', heldValue);
});

const target = { name: 'Alice' };
registry.register(target, { id: 42 });

// The target is collected; the held value { id: 42 } stays alive

The target is held weakly:

Registration does not prevent the target from being collected.

Unregistering:

registry.unregister(token);

Provide an unregister token at registration:

const token = {};
registry.register(target, 'held', token);

registry.unregister(token);

You can’t unregister by target — you must provide a token.

Complete example:

const registry = new FinalizationRegistry((heldValue) => {
  console.log('Cleaning up:', heldValue);
});

const token = {};

let obj = { name: 'Alice' };
registry.register(obj, 'obj-1', token);

// Cancel before collection
registry.unregister(token);

obj = null;
// No cleanup log — we unregistered

Multiple registrations:

You can register the same object multiple times, and even register the same held value with different targets:

registry.register(objA, 'A');
registry.register(objB, 'B');

Each fires independently.

Common FinalizationRegistry patterns:

Pattern 1 — Resource cleanup:

const finalizer = new FinalizationRegistry(({ type, id }) => {
  console.log(`Auto-closing ${type} ${id}`);
});

class FileHandle {
  constructor(path) {
    this.path = path;
    this.closed = false;
    finalizer.register(this, { type: 'file', id: path });
  }
  close() {
    if (!this.closed) {
      this.closed = true;
      finalizer.unregister(this);   // need token — see next pattern
      console.log(`Closed ${this.path}`);
    }
  }
}

To unregister, you need a token. Store it:

class FileHandle {
  constructor(path) {
    this.path = path;
    this.closed = false;
    this.token = {};
    finalizer.register(this, { type: 'file', id: path }, this.token);
  }
  close() {
    if (!this.closed) {
      this.closed = true;
      finalizer.unregister(this.token);
      console.log(`Closed ${this.path}`);
    }
  }
}

Pattern 2 — Tracking object counts:

let alive = 0;

const counter = new FinalizationRegistry(() => {
  alive--;
  console.log('Remaining:', alive);
});

function create() {
  const obj = {};
  alive++;
  counter.register(obj, null);
  return obj;
}

Pattern 3 — Detecting leaks:

const registry = new FinalizationRegistry((id) => {
  console.log(`Object ${id} was collected`);
});

const objects = new Map();
let nextId = 0;

function track(obj) {
  const id = nextId++;
  objects.set(id, new WeakRef(obj));
  registry.register(obj, id);
  return id;
}

Pattern 4 — Cleanup with side effects:

const registry = new FinalizationRegistry((filePath) => {
  fs.unlink(filePath, () => {});
});

function withTempFile(data) {
  const path = `/tmp/${Date.now()}`;
  fs.writeFileSync(path, data);
  const handle = { path };
  registry.register(handle, path);
  return handle;
}

Pattern 5 — Clearing caches:

const cache = new Map();

const registry = new FinalizationRegistry((key) => {
  cache.delete(key);
});

function cacheWith(obj, key) {
  cache.set(key, obj);
  registry.register(obj, key);
}

When to use FinalizationRegistry:

Use caseExample
Freeing native memoryWASM, WebGL
Closing file descriptorsFile handles
Releasing pooled resourcesConnection pools
Testing memoryDetecting leaks
Side-effect cleanupTemp file deletion

When NOT to use FinalizationRegistry:

  • Critical cleanup — GC might not run before the program exits
  • Deterministic behavior — use explicit close() methods
  • Frequent cleanup — the callback overhead adds up
  • Web Workers or old browsers — check support

FinalizationRegistry is non-deterministic:

let obj = {};
const registry = new FinalizationRegistry(() => {
  console.log('collected');
});

registry.register(obj, null);
obj = null;

console.log('before');
// [ 'before' ]
// ...eventually: [ 'collected' ]

console.log('after');
// [ 'after' ]

// But the "collected" line might never print during this run

If GC doesn’t run before the program exits, the callback never fires.

The registry and its held values keep data alive:

Held values are stored strongly by the registry:

const registry = new FinalizationRegistry((hugeData) => {});
registry.register(obj, new Array(1e6).fill(0));

The big array stays alive until the target is collected. Choose held values carefully.

Browser and Node.js support:

EnvironmentSupport
Node.js✅ (v14.6.0+)
Chrome
Firefox✅ (v79+)
Safari✅ (v14.1+)
Edge

Combining WeakRef and FinalizationRegistry:

const registry = new FinalizationRegistry((key) => {
  cache.delete(key);
  console.log(`Cache cleared: ${key}`);
});

const cache = new Map();

function get(key, factory) {
  const ref = cache.get(key);
  if (ref) {
    const value = ref.deref();
    if (value) return value;
    cache.delete(key);   // stale
  }

  const value = factory();
  cache.set(key, new WeakRef(value));
  registry.register(value, key);
  return value;
}

This is a weak cache with a self-cleaning Map. When an object is collected, the registry removes its entry from the cache.

Warning from the spec:

“The FinalizationRegistry is not a substitute for proper resource management. It may call the callback long after the object is no longer useful, or never.”

Translation: use it as a safety net, not as a primary cleanup mechanism.


c – Practical patterns and pitfalls

Both APIs are easy to misuse. Here are the patterns that work and the mistakes to avoid.

Pattern 1 — Cache with weak values:

function createWeakCache() {
  const map = new Map();

  return {
    get(key) {
      const ref = map.get(key);
      const value = ref?.deref();
      if (!value) {
        map.delete(key);
        return undefined;
      }
      return value;
    },
    set(key, value) {
      map.set(key, new WeakRef(value));
    }
  };
}

The Map doesn’t keep values alive — only WeakRefs do.

Pattern 2 — Weak value store:

class WeakValueStore {
  #refs = new Map();

  set(key, value) {
    this.#refs.set(key, new WeakRef(value));
  }

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

  has(key) {
    return this.#refs.get(key)?.deref() !== undefined;
  }

  cleanup() {
    for (const [key, ref] of this.#refs) {
      if (!ref.deref()) this.#refs.delete(key);
    }
  }
}

Periodic cleanup removes stale entries.

Pattern 3 — Cleanup registry with unregister tokens:

class Resource {
  #token = {};

  constructor() {
    finalRegistry.register(this, this, this.#token);
  }

  destroy() {
    finalRegistry.unregister(this.#token);
  }
}

Pattern 4 — Tracking alive objects:

const alive = new Map();
let count = 0;

const registry = new FinalizationRegistry((id) => {
  alive.delete(id);
  count--;
});

function track(obj) {
  const id = count++;
  alive.set(id, obj);
  registry.register(obj, id);
  return id;
}

Pattern 5 — Timed cache with WeakRef:

class TimedCache {
  #refs = new Map();

  set(key, value, ttl) {
    this.#refs.set(key, {
      ref: new WeakRef(value),
      expires: Date.now() + ttl
    });
  }

  get(key) {
    const entry = this.#refs.get(key);
    if (!entry) return undefined;
    if (entry.expires < Date.now()) {
      this.#refs.delete(key);
      return undefined;
    }
    const value = entry.ref.deref();
    if (!value) this.#refs.delete(key);
    return value;
  }
}

TTL plus weak reference — object dies when either expires or is collected.

Pattern 6 — Observing collection in tests:

function detectLeaks() {
  const collected = [];
  const registry = new FinalizationRegistry((id) => {
    collected.push(id);
  });

  return { registry, collected };
}

Useful for library tests to confirm objects aren’t leaked.

Pattern 7 — WASM memory cleanup:

const wasmFinalizer = new FinalizationRegistry((ptr) => {
  wasmModule.free(ptr);
});

function withWasmObject() {
  const obj = {};
  const ptr = wasmModule.alloc();
  wasmFinalizer.register(obj, ptr);
  return obj;
}

When obj is collected, WASM memory is freed.

Pattern 8 — WebGL resource cleanup:

const glFinalizer = new FinalizationRegistry((buffer) => {
  gl.deleteBuffer(buffer);
});

function createBuffer() {
  const holder = {};
  const buffer = gl.createBuffer();
  glFinalizer.register(holder, buffer);
  return holder;
}

Pitfall 1 — WeakRef doesn’t force GC:

let obj = { data: new Array(1e6).fill(0) };
const ref = new WeakRef(obj);
obj = null;

// This may still be alive
console.log(ref.deref());
// [ { data: [...] } ]

The GC decides when to run. deref() might return the object for a while.

Pitfall 2 — Can’t unregister by target:

registry.register(target, held);
registry.unregister(target);
// ❌ — unregister needs a token, not the target

You must provide the token at registration time and use that.

Pitfall 3 — Held value keeps memory alive:

registry.register(obj, bigArray);

bigArray stays alive until obj is collected. Use minimal held values.

Pitfall 4 — Callbacks run at unpredictable times:

registry.register(obj, () => {
  closeConnection();
});

You can’t predict when closeConnection() runs — it might be seconds later, minutes later, or never.

Pitfall 5 — Don’t use for critical cleanup:

// ❌ Don't rely on this to flush a file
registry.register(file, () => {
  file.close();
});
// ✅ Do provide explicit close()
file.close();

Always provide a manual cleanup method. The finalizer is a safety net.

Pitfall 6 — Memory leak in the registry itself:

for (let i = 0; i < 1e6; i++) {
  registry.register({}, i);
}

Registrations pile up until their targets are collected. If targets are short-lived, the registry grows until the GC runs.

Pitfall 7 — Async cleanup:

registry.register(obj, async () => {
  await cleanup();
});

Finalizers should be synchronous. Async cleanup can leak or be skipped.

Pitfall 8 — Using FinalizationRegistry for control flow:

// ❌ Don't do this
let done = false;
registry.register(obj, () => { done = true; });

while (!done) { /* wait */ }
// Hangs forever if GC never runs

Never wait on a finalizer.

Pitfall 9 — Multiple registries for the same object:

registryA.register(obj, 'A');
registryB.register(obj, 'B');

Both fire, in no guaranteed order.

Pitfall 10 — WeakRef on primitives:

new WeakRef(42);
// TypeError

Only objects.

Pitfall 11 — Reusing the same held value:

registry.register(objA, sharedState);
registry.register(objB, sharedState);

The callback runs twice with the same held value. Make sure your callback handles that.

Pitfall 12 — GC timing differs across engines:

// Browser A might run GC at time X
// Browser B might run it at time Y
// Node might run it at time Z

Never write code that depends on GC timing.

Pitfall 13 — FinalizationRegistry callbacks and microtasks:

registry.register(obj, () => {
  queueMicrotask(() => console.log('after'));
});

The callback runs during a GC microtask — you can schedule more work, but keep it simple.

Pitfall 14 — WeakRef and function closures:

function makeCounter() {
  let count = 0;
  const ref = new WeakRef(() => count++);
  return ref;
}

The closure is referenced by the WeakRef. If nothing else holds it, it’s collectible.

Pitfall 15 — Circular references and GC:

Modern GC handles cycles:

const a = {};
const b = {};
a.b = b;
b.a = a;

a and b will still be collected when no external references remain.

Good practice — always provide explicit cleanup:

class Connection {
  constructor() {
    this.closed = false;
    registry.register(this, this, this);
  }
  close() {
    if (this.closed) return;
    this.closed = true;
    registry.unregister(this);
    // real close logic
  }
}

The finalizer is a fallback if the caller forgets. The explicit close() is the primary path.

When to reach for WeakRef:

  • You’re building a cache that shouldn’t prevent GC
  • You’re tracking objects weakly
  • You want to avoid strong references in long-lived structures

When to reach for FinalizationRegistry:

  • You need to release native resources (WASM, WebGL, files)
  • You want to detect when cached objects are collected
  • You’re writing a library that must clean up external state
  • You want to observe leaks in tests

When neither is the right tool:

  • Deterministic cleanup — use try/finally or explicit close()
  • Ownership tracking — use a strong reference
  • General caching — use Map with an eviction policy (LRU)
  • Reference counting — not applicable in JS

The most important rule:

WeakRef and FinalizationRegistry are advanced tools for infrastructure. If you’re not writing library internals, you probably don’t need them. And even then, always provide explicit cleanup.


Complete Example Session

// ============================================
// PART 1: BASIC WEAKREF
// ============================================

let obj = { id: 1 };
const ref = new WeakRef(obj);

console.log(ref.deref());
// [ { id: 1 } ]

console.log(ref.deref() === obj);
// [ true ]

// ============================================
// PART 2: AFTER STRONG REF DROPS
// ============================================

obj = null;

console.log(ref.deref());
// [ { id: 1 } ]  ← still alive — GC hasn't run

setTimeout(() => {
  console.log(ref.deref());
  // [ undefined ] or [ { id: 1 } ] depending on GC
}, 1000);

// ============================================
// PART 3: WEAKREF REQUIRES OBJECT
// ============================================

try {
  new WeakRef(42);
} catch (err) {
  console.log(err.message);
}
// [ Invalid value used in weak ref ]

// ============================================
// PART 4: WEAK CACHE
// ============================================

class Cache {
  #refs = new Map();

  get(key) {
    const ref = this.#refs.get(key);
    const value = ref?.deref();
    if (!value) this.#refs.delete(key);
    return value;
  }

  set(key, value) {
    this.#refs.set(key, new WeakRef(value));
  }
}

const cache = new Cache();
let cached = { data: 'x' };
cache.set('a', cached);
console.log(cache.get('a')?.data);
// [ 'x' ]

// ============================================
// PART 5: FINALIZATIONREGISTRY
// ============================================

const registry = new FinalizationRegistry((heldValue) => {
  console.log('Collected:', heldValue);
});

let target = { name: 'Alice' };
registry.register(target, 'target-object');
target = null;

setTimeout(() => {
  console.log('done');
}, 5000);

// ============================================
// PART 6: HELD VALUE
// ============================================

const heldValue = { id: 42 };
const registry2 = new FinalizationRegistry((value) => {
  console.log('Held value:', value.id);
});

let t2 = {};
registry2.register(t2, heldValue);
t2 = null;

// ============================================
// PART 7: UNREGISTER WITH TOKEN
// ============================================

const registry3 = new FinalizationRegistry((v) => {
  console.log('Should not fire:', v);
});

const token = {};
let t3 = {};
registry3.register(t3, 'x', token);
registry3.unregister(token);

t3 = null;
// No callback fires

// ============================================
// PART 8: RESOURCE CLEANUP
// ============================================

class Resource {
  constructor(id) {
    this.id = id;
    this.token = {};
    registry.register(this, id, this.token);
  }
  release() {
    console.log('Explicit release:', this.id);
    registry.unregister(this.token);
  }
}

{
  let r = new Resource('res-1');
  r.release();
  // [ 'Explicit release: res-1' ]
}

// ============================================
// PART 9: WEAKREF + FINALIZATION
// ============================================

const cleanupCache = new Map();

const registry4 = new FinalizationRegistry((key) => {
  cleanupCache.delete(key);
  console.log('Auto-cleaned:', key);
});

function cacheSet(key, value) {
  cleanupCache.set(key, new WeakRef(value));
  registry4.register(value, key);
}

let v = { data: 1 };
cacheSet('k', v);
console.log(cleanupCache.has('k'));
// [ true ]

// ============================================
// PART 10: NOT PREDICTABLE
// ============================================

let obj4 = {};
const ref4 = new WeakRef(obj4);
obj4 = null;

console.log(ref4.deref() !== undefined);
// [ true ] — object still alive

// GC timing is not guaranteed

// ============================================
// PART 11: NO PRIMITIVES
// ============================================

try {
  new WeakRef('hello');
} catch (err) {
  console.log(err.message);
}
// [ Invalid value used in weak ref ]

// ============================================
// PART 12: SAME OBJECT MULTIPLE REGISTRATIONS
// ============================================

const registry5 = new FinalizationRegistry((v) => console.log(v));

let obj5 = {};
registry5.register(obj5, 'A');
registry5.register(obj5, 'B');

obj5 = null;
// Eventually both callbacks fire

// ============================================
// PART 13: HELD VALUE KEEPS MEMORY ALIVE
// ============================================

const registry6 = new FinalizationRegistry(() => {});
let big = { data: new Array(1e6).fill(0) };
registry6.register({}, big);   // ❌ big kept alive until target dies

// ============================================
// PART 14: SAFETY NET PATTERN
// ============================================

class File {
  #token = {};
  #closed = false;

  constructor(path) {
    this.path = path;
    finalRegistry.register(this, this.path, this.#token);
  }

  close() {
    if (this.#closed) return;
    this.#closed = true;
    finalRegistry.unregister(this.#token);
    console.log('Explicit close:', this.path);
  }
}

// ============================================
// PART 15: TRACKING COUNTS
// ============================================

let alive = 0;

const countRegistry = new FinalizationRegistry(() => {
  alive--;
});

function track() {
  const obj = {};
  alive++;
  countRegistry.register(obj, null);
  return obj;
}

// ============================================
// PART 16: WEAKREF IN MAP
// ============================================

const store = new Map();

function weakSet(key, value) {
  store.set(key, new WeakRef(value));
}

function weakGet(key) {
  return store.get(key)?.deref();
}

let val = { x: 1 };
weakSet('a', val);
console.log(weakGet('a').x);
// [ 1 ]

// ============================================
// PART 17: WEAKREF DOES NOT PREVENT COLLECTION
// ============================================

function test() {
  let local = { name: 'temp' };
  const ref = new WeakRef(local);
  return ref;
}

const returned = test();
// local is out of scope; only the WeakRef remains
// GC may collect it at any time

// ============================================
// PART 18: COMBINED CACHE
// ============================================

class WeakCache {
  #map = new Map();
  #finalizer;

  constructor() {
    this.#finalizer = new FinalizationRegistry((key) => {
      this.#map.delete(key);
    });
  }

  set(key, value) {
    this.#map.set(key, new WeakRef(value));
    this.#finalizer.register(value, key);
  }

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

// ============================================
// PART 19: UNREGISTER SAVES MEMORY
// ============================================

const r = new FinalizationRegistry(() => {});

let saved = {};
const t = {};
r.register(saved, 'huge-held-value', t);

r.unregister(t);
// held value is freed when unregistered

// ============================================
// PART 20: FULL SCRIPT
// ============================================

let obj59 = { id: 1 };
const ref59 = new WeakRef(obj59);

console.log(ref59.deref());
console.log(ref59.deref() === obj59);

obj59 = null;

setTimeout(() => {
  console.log(ref59.deref());
}, 1000);

const registry59 = new FinalizationRegistry((heldValue) => {
  console.log('Collected:', heldValue);
});

let target59 = { name: 'Alice' };
registry59.register(target59, 'target-object');
target59 = null;

setTimeout(() => {
  console.log('After GC check');
}, 5000);

class Resource59 {
  constructor(id) {
    this.id = id;
  }
  release() {
    console.log('Releasing', this.id);
  }
}

const finalizer59 = new FinalizationRegistry((id) => {
  console.log('Resource freed:', id);
});

let resource59 = new Resource59('db-connection');
finalizer59.register(resource59, resource59.id);
resource59 = null;

Quick Reference

WeakRef

MethodPurpose
new WeakRef(obj)Create weak reference
ref.deref()Get object or undefined

FinalizationRegistry

MethodPurpose
new FinalizationRegistry(cb)Create registry
registry.register(target, held, token?)Register
registry.unregister(token)Cancel

WeakRef vs Other

FeatureWeakRefWeakMapWeakSetMap
HoldsObjectObject → valueObjectAny → any
Retrieval.deref().get(k).has(v).get(k)
Iterable
Size
ClearsOn GCOn GCOn GCManual

WeakMap vs WeakRef

Use caseBetter choice
Metadata for objectWeakMap
Cache values by objectWeakMap
Weak pointer to objectWeakRef
Track alive objectsWeakRef

FinalizationRegistry vs Alternatives

TaskTool
Explicit cleanuptry/finally
Native resourceFinalizationRegistry
Cache evictionLRU Map
Leak detectionFinalizationRegistry

When to Use

Use WeakRef whenUse FinalizationRegistry when
You need a weak cacheYou release native memory
You track objects weaklyYou clean up side effects
You avoid leaksYou observe collection

When NOT to Use

Don’t use WeakRef whenDon’t use FinalizationRegistry when
You need the objectCleanup must be deterministic
You need deterministic behaviorTask must run before exit
A normal Map worksA try/finally works

Browser / Node Support

EnvironmentWeakRefFinalizationRegistry
Node.js 14.6+
Chrome 84+
Firefox 79+
Safari 14.1+

Best Practices

Do This:

// Always check deref result
const value = ref.deref();
if (value) { /* use value */ }                   // ✅

// Provide explicit cleanup
class Res {
  close() {
    this.#closed = true;
    registry.unregister(this.#token);
  }
}                                                 // ✅

// Use tokens to unregister
const token = {};
registry.register(obj, held, token);
registry.unregister(token);                       // ✅

// Keep held values minimal
registry.register(obj, id);                       // ✅

// Use WeakMap for metadata
const meta = new WeakMap();                       // ✅

// Use WeakRef for weak caches
map.set(key, new WeakRef(value));                 // ✅

// Detect leaks in tests
const registry = new FinalizationRegistry(...)    // ✅

Don’t Do This:

// Don't rely on GC timing
await waitForGC();                                // ❌ impossible

// Don't use WeakRef for critical data
const ref = new WeakRef(config);                  // ⚠️  config may vanish

// Don't use primitives
new WeakRef(42);                                  // ❌ TypeError

// Don't forget held values hold memory
registry.register(obj, bigArray);                 // ⚠️  bigArray stays

// Don't unregister without token
registry.unregister(obj);                         // ❌ wrong API

// Don't do async cleanup in finalizer
registry.register(obj, async () => {});           // ⚠️  may not finish

// Don't use for control flow
while (!collected) {}                             // ❌ hangs

// Don't expect immediate collection
obj = null;
ref.deref();                                      // ⚠️  may return object

// Don't use FinalizationRegistry for critical cleanup
// Always provide an explicit close()

Common Pitfalls

PitfallProblemSolution
Timing assumptionGC is non-deterministicNever rely on timing
Primitive WeakRefTypeErrorObjects only
Held values hold memoryLeaksKeep them small
Unregister without tokenWrong APIStore the token
Async finalizerMay not runKeep synchronous
Critical cleanupGC may skipExplicit close
Waiting on GCHangsNever block
Many registrationsMemory growthUnregister when done
Proxy of WeakRefWeird behaviorDon’t proxy them
deref() uncheckedundefined errorsAlways check

Real-World Examples

1. Basic WeakRef

let obj = { id: 1 };
const ref = new WeakRef(obj);
console.log(ref.deref().id);
// [ 1 ]

2. Deref After Null

let obj = { id: 1 };
const ref = new WeakRef(obj);
obj = null;
console.log(ref.deref() !== undefined);
// [ true ] or [ false ] — depends on GC

3. WeakRef Requires Object

try {
  new WeakRef(42);
} catch (err) {
  console.log(err.message);
}
// [ Invalid value used in weak ref ]

4. Weak Cache

const cache = new Map();
function set(key, value) {
  cache.set(key, new WeakRef(value));
}

5. Deref and Delete Stale

function get(key) {
  const ref = cache.get(key);
  const value = ref?.deref();
  if (!value) cache.delete(key);
  return value;
}

6. Basic Registry

const registry = new FinalizationRegistry((held) => {
  console.log('gone:', held);
});

let obj = {};
registry.register(obj, 'id-1');
obj = null;

7. Unregister

const token = {};
registry.register(obj, 'held', token);
registry.unregister(token);

8. Resource Cleanup

class File {
  constructor(path) {
    this.path = path;
    this.token = {};
    registry.register(this, path, this.token);
  }
  close() {
    registry.unregister(this.token);
  }
}

9. Count Objects

let alive = 0;
const registry = new FinalizationRegistry(() => alive--);

function create() {
  const obj = {};
  alive++;
  registry.register(obj, null);
  return obj;
}

10. Combined Weak Cache

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

  set(k, v) {
    this.#map.set(k, new WeakRef(v));
    this.#finalizer.register(v, k);
  }

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

11. Tracking Alive Objects

const alive = new Map();
let nextId = 0;

const registry = new FinalizationRegistry((id) => {
  alive.delete(id);
});

function track(obj) {
  const id = nextId++;
  alive.set(id, obj);
  registry.register(obj, id);
}

12. Leak Detection

const registry = new FinalizationRegistry((id) => {
  console.log('Leaked:', id);
});

for (let i = 0; i < 100; i++) {
  registry.register({}, i);
}

13. WASM Cleanup

const wasmFinalizer = new FinalizationRegistry((ptr) => {
  wasm.free(ptr);
});

function makeWasmObj() {
  const holder = {};
  wasmFinalizer.register(holder, wasm.alloc());
  return holder;
}

14. WebGL Buffer

const glFinalizer = new FinalizationRegistry((buffer) => {
  gl.deleteBuffer(buffer);
});

15. Held Value Keeps Memory

const big = new Array(1e6).fill(0);
registry.register({}, big);   // big stays alive

16. Multiple Registrations

registry.register(obj, 'A');
registry.register(obj, 'B');
// Both fire when obj is collected

17. Explicit Close Preferred

class Conn {
  close() {
    // primary cleanup
  }
  constructor() {
    registry.register(this, this);   // fallback only
  }
}

18. WeakRef in Map

const map = new Map();
map.set('k', new WeakRef({ data: 1 }));
const value = map.get('k')?.deref();

19. Deref Check

const value = ref.deref();
if (value) {
  console.log(value);
} else {
  console.log('collected');
}

20. Full Script

let obj59 = { id: 1 };
const ref59 = new WeakRef(obj59);

console.log(ref59.deref());
console.log(ref59.deref() === obj59);

obj59 = null;

setTimeout(() => {
  console.log(ref59.deref());
}, 1000);

const registry59 = new FinalizationRegistry((heldValue) => {
  console.log('Collected:', heldValue);
});

let target59 = { name: 'Alice' };
registry59.register(target59, 'target-object');
target59 = null;

setTimeout(() => {
  console.log('After GC check');
}, 5000);

class Resource59 {
  constructor(id) {
    this.id = id;
  }
  release() {
    console.log('Releasing', this.id);
  }
}

const finalizer59 = new FinalizationRegistry((id) => {
  console.log('Resource freed:', id);
});

let resource59 = new Resource59('db-connection');
finalizer59.register(resource59, resource59.id);
resource59 = null;

Visual: WeakRef Lifecycle

┌──────────────────────────────────────────────┐
│  let obj = { data: 1 };                      │
│  const ref = new WeakRef(obj);               │
│                                              │
│  obj ──► { data: 1 }  ← strong               │
│  ref ──► { data: 1 }  ← weak (deref)         │
│                                              │
│  Object alive, deref returns it              │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  obj = null;                                 │
│                                              │
│  ref ──► { data: 1 }  ← weak only            │
│                                              │
│  GC may collect at any time                  │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  After GC:                                   │
│                                              │
│  ref ──► (nothing)                           │
│                                              │
│  ref.deref() → undefined                     │
│                                              │
└──────────────────────────────────────────────┘

Visual: FinalizationRegistry Flow

┌──────────────────────────────────────────────┐
│  const registry = new FinalizationRegistry(  │
│    (held) => console.log(held)               │
│  );                                          │
│                                              │
│  let obj = {};                               │
│  registry.register(obj, 'my-id');            │
│                                              │
│  Object → registry                           │
│    └── held value: 'my-id'                   │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  obj = null;                                 │
│                                              │
│  GC runs → object collected                  │
│                                              │
│  registry callback fires:                    │
│    console.log('my-id')                      │
│                                              │
│  Timing: whenever GC decides                 │
│                                              │
└──────────────────────────────────────────────┘

Visual: WeakRef vs Strong Reference

┌──────────────────────────────────────────────┐
│  Strong reference                            │
│                                              │
│  variable ──► object                         │
│                                              │
│  Object stays alive as long as variable      │
│  points to it                                 │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Weak reference                              │
│                                              │
│  WeakRef ──► object                          │
│  (doesn't count)                             │
│                                              │
│  Object can be collected even if WeakRef     │
│  points to it                                │
│                                              │
└──────────────────────────────────────────────┘

Summary

ConceptSyntaxPurpose
WeakRefnew WeakRef(obj)Weak reference
Derefref.deref()Get or undefined
Registrynew FinalizationRegistry(cb)Collection callback
Registerregistry.register(t, held, token?)Register
Unregisterregistry.unregister(token)Cancel
Held valueSecond arg to registerData for callback
TokenThird arg to registerUnregister key

Key takeaways:

  • WeakRef holds a weak reference — doesn’t prevent GC
  • .deref() returns the object or undefined
  • Works only on objects — not primitives
  • FinalizationRegistry runs a callback after an object is collected
  • Registration is weak — doesn’t keep the target alive
  • Held values stay alive until the target is collected
  • Use tokens to unregister before collection
  • GC timing is not deterministic — never rely on when callbacks fire
  • Never use for critical cleanup — always provide explicit close()
  • Use WeakRef for weak caches that shouldn’t prevent collection
  • Use FinalizationRegistry for native resource cleanup (WASM, WebGL, files)
  • WeakMap is often better than WeakRef for metadata association
  • Don’t over-engineer — if a normal Map or explicit cleanup works, use it

Remember: WeakRef and FinalizationRegistry are advanced tools for infrastructure code — library internals, memory-sensitive caches, and native resource management. They exist so you can hold a hint about an object or clean up when it disappears, without preventing collection. But they’re non-deterministic, easy to misuse, and rarely the right answer for application code. Use them sparingly, always provide explicit cleanup, and never depend on the GC to do something at a specific time.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!