Skip to content
Codeloom
JavaScript

FinalizationRegistry and WeakRef in JavaScript

Learn how to use WeakRef and FinalizationRegistry for custom cleanup callbacks, weak caching, and observing garbage collection in JavaScript.

·6 min read · By Codeloom
Advanced 10 min read

What you'll learn

  • How WeakRef creates references that do not prevent garbage collection
  • Using FinalizationRegistry to run cleanup when objects are collected
  • Building weak caches and resource managers with these APIs

Prerequisites

  • JavaScript objects and memory management basics
  • Understanding of garbage collection concepts
  • Familiarity with WeakMap and WeakSet

JavaScript is a garbage-collected language — objects are automatically freed when no strong references point to them. WeakRef and FinalizationRegistry, introduced in ES2021, give you controlled interaction with the garbage collector: weak references that do not prevent collection, and callbacks that fire when objects are collected.

WeakRef basics

A WeakRef wraps an object without preventing it from being garbage collected. You access the underlying object via .deref(), which returns undefined if the object has been collected.

let largeData = { values: new Array(1_000_000).fill(0) };

const weakRef = new WeakRef(largeData);

console.log(weakRef.deref()); // { values: [...] } -- still alive

// Remove the strong reference
largeData = null;

// At some point after GC runs:
// weakRef.deref() will return undefined

Important: garbage collection timing is non-deterministic. You cannot predict exactly when deref() will start returning undefined. The spec guarantees that within a single synchronous execution (a single “turn” of the event loop), deref() will return a consistent result.

Building a weak cache

The most practical use of WeakRef is building a cache that does not prevent its entries from being garbage collected when memory pressure is high.

class WeakCache {
  #cache = new Map();

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

  get(key) {
    const ref = this.#cache.get(key);
    if (!ref) return undefined;

    const value = ref.deref();
    if (value === undefined) {
      // Object was garbage collected, clean up the entry
      this.#cache.delete(key);
      return undefined;
    }

    return value;
  }

  has(key) {
    return this.get(key) !== undefined;
  }
}
const cache = new WeakCache();

function fetchAndCache(url) {
  const cached = cache.get(url);
  if (cached) {
    console.log("Cache hit");
    return cached;
  }

  console.log("Cache miss -- fetching");
  const data = { url, timestamp: Date.now() }; // Simulated fetch result
  cache.set(url, data);
  return data;
}

let result = fetchAndCache("/api/users"); // Cache miss
result = fetchAndCache("/api/users");     // Cache hit

The weak cache allows the garbage collector to reclaim cached objects under memory pressure, preventing the cache from growing unbounded.

FinalizationRegistry

FinalizationRegistry lets you register a callback that fires when a registered object is garbage collected. This is useful for cleaning up external resources associated with an object.

const registry = new FinalizationRegistry((heldValue) => {
  console.log(`Object associated with "${heldValue}" was collected`);
});

let obj = { name: "temporary" };
registry.register(obj, "my-object");

// Remove the strong reference
obj = null;

// Eventually, after GC: "Object associated with 'my-object' was collected"

The second argument to register() is the “held value” — data passed to the callback. It must not be the registered object itself (that would create a strong reference and prevent collection).

Unregistering

You can pass an unregister token to register() and later call unregister() to cancel the callback.

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

let obj = { data: "important" };
const token = {}; // Any object can serve as the token

registry.register(obj, "resource-123", token);

// If we clean up manually, unregister to prevent the callback
registry.unregister(token);

Practical example: file handle manager

Suppose you have objects that hold external resources (like file handles or database connections). FinalizationRegistry ensures cleanup even if the developer forgets to call a close() method.

class FileHandleManager {
  #registry;
  #openHandles = new Map();

  constructor() {
    this.#registry = new FinalizationRegistry((handleId) => {
      console.warn(`File handle ${handleId} was not closed -- cleaning up`);
      this.#closeHandle(handleId);
    });
  }

  open(filePath) {
    const handleId = crypto.randomUUID();
    const handle = { id: handleId, path: filePath, fd: this.#openFile(filePath) };
    const token = { handleId };

    this.#openHandles.set(handleId, { fd: handle.fd, token });
    this.#registry.register(handle, handleId, token);

    return handle;
  }

  close(handle) {
    const entry = this.#openHandles.get(handle.id);
    if (entry) {
      this.#closeHandle(handle.id);
      this.#registry.unregister(entry.token);
    }
  }

  #openFile(path) {
    // Simulated file open
    return Math.floor(Math.random() * 1000);
  }

  #closeHandle(handleId) {
    const entry = this.#openHandles.get(handleId);
    if (entry) {
      console.log(`Closing file descriptor ${entry.fd}`);
      this.#openHandles.delete(handleId);
    }
  }
}
const manager = new FileHandleManager();

// Good: explicit cleanup
const handle1 = manager.open("/tmp/data.txt");
manager.close(handle1);

// Bad but safe: forgot to close -- FinalizationRegistry cleans up
let handle2 = manager.open("/tmp/leak.txt");
handle2 = null; // Eventually triggers cleanup callback

Combining WeakRef and FinalizationRegistry

A robust weak cache uses both: WeakRef for the cache entries and FinalizationRegistry to automatically remove stale map keys.

class AutoCleaningCache {
  #cache = new Map();
  #registry;

  constructor() {
    this.#registry = new FinalizationRegistry((key) => {
      // Only delete if the current entry is still the collected one
      const ref = this.#cache.get(key);
      if (ref && ref.deref() === undefined) {
        this.#cache.delete(key);
      }
    });
  }

  set(key, value) {
    const existingRef = this.#cache.get(key);
    if (existingRef) {
      const existing = existingRef.deref();
      if (existing) {
        this.#registry.unregister(existing);
      }
    }

    this.#cache.set(key, new WeakRef(value));
    this.#registry.register(value, key, value);
  }

  get(key) {
    const ref = this.#cache.get(key);
    return ref?.deref();
  }

  get size() {
    return this.#cache.size;
  }
}

This cache automatically shrinks as objects are garbage collected, without requiring manual cleanup or periodic sweeps.

Important caveats

These APIs come with significant caveats that you must understand before using them.

Non-deterministic timing. You cannot rely on when (or even if) a finalization callback will run. The garbage collector decides when to collect objects. Callbacks may be batched, delayed, or in some edge cases, never called (for example, if the page is closed before GC runs).

Do not use for correctness. FinalizationRegistry is a safety net, not a primary cleanup mechanism. Always provide explicit cleanup methods (like close() or dispose()) and use finalization only as a fallback.

Performance implications. Registering many objects with a FinalizationRegistry has overhead. The garbage collector must track registered objects and schedule callbacks.

WeakRef consistency. Within a single synchronous execution, deref() returns consistent results. But across event loop turns, the object may be collected between calls.

Summary

WeakRef and FinalizationRegistry provide controlled interaction with JavaScript’s garbage collector:

  • WeakRef holds a reference that does not prevent garbage collection. Use .deref() to access the object, which returns undefined after collection.
  • FinalizationRegistry runs a callback when a registered object is collected, enabling automatic resource cleanup.
  • Combining both enables self-cleaning caches that shrink under memory pressure.
  • These APIs are safety nets, not primary cleanup mechanisms. Always provide explicit disposal methods.
  • Garbage collection timing is non-deterministic. Never rely on finalization for program correctness.