Skip to content

WeakMap, WeakSet, WeakRef, and FinalizationRegistry

advanced14 min read

The Problem: References That Keep Things Alive Too Long

You've learned that any reachable reference prevents garbage collection. But here's a scenario that comes up constantly: you want to associate data with an object — metadata, cached computations, DOM element state — without preventing the object from being collected when it's no longer needed elsewhere.

// PROBLEM: This map keeps every user object alive forever
const userMetadata = new Map();

function trackUser(user) {
  userMetadata.set(user, {
    firstSeen: Date.now(),
    interactions: 0
  });
}

// Even if the original 'user' object is no longer used anywhere,
// the Map key keeps it reachable: global → userMetadata → key (user) → user object
// The metadata entry AND the user both leak

You need a way to say: "store this association, but if the key object is garbage collected by everyone else, let the entry disappear too." Sounds reasonable, right?

That's exactly what weak references provide.

WeakMap: Metadata Without Ownership

Mental Model

Think of a WeakMap as sticky notes attached to objects. The sticky note (value) is associated with the object (key), but the note doesn't prevent the object from being thrown away. When the object goes to the trash, the sticky note disappears with it. You can't list all the sticky notes — you can only check a specific object for its note.

A WeakMap is like a Map, but with critical differences:

PropertyMapWeakMap
Key typesAny valueObjects and Symbols only
Prevents GC of keysYesNo
IterableYes (.keys(), .values(), .entries(), for...of)No
.size propertyYesNo
Use caseGeneral key-value storageAssociating metadata with objects you don't own
const metadata = new WeakMap();

function processElement(element) {
  // Associate data with the element without preventing its collection
  if (!metadata.has(element)) {
    metadata.set(element, {
      processedAt: Date.now(),
      computedLayout: computeExpensiveLayout(element)
    });
  }
  return metadata.get(element);
}

// When 'element' is removed from the DOM and all other references are gone,
// the WeakMap entry is automatically removed — no manual cleanup needed

Why WeakMap Can't Be Iterated

This trips people up. "Why can't I loop over a WeakMap?" It's a deliberate design choice, not a limitation. If you could iterate a WeakMap's keys, you'd hold references to all of them during iteration — which would prevent garbage collection, defeating the entire purpose. The GC must be free to collect any key at any time, so the WeakMap cannot expose its key set.

Real-World WeakMap Patterns

Private data for class instances:

const _private = new WeakMap();

class User {
  constructor(name, email) {
    _private.set(this, { email, passwordHash: null });
    this.name = name;
  }

  getEmail() {
    return _private.get(this).email;
  }
}

const user = new User('Alice', 'alice@example.com');
user.name;       // 'Alice' — public
user.email;      // undefined — not on the instance
user.getEmail(); // 'alice@example.com' — accessible through WeakMap

// When 'user' is GC'd, the private data is automatically cleaned up

Caching computed results per object:

const computeCache = new WeakMap();

function expensiveComputation(obj) {
  if (computeCache.has(obj)) return computeCache.get(obj);

  const result = /* ... heavy computation ... */ obj.data.reduce((a, b) => a + b, 0);
  computeCache.set(obj, result);
  return result;
}

// Cache entries disappear when the input object is GC'd
// No manual cache invalidation needed

WeakSet: Membership Without Ownership

A WeakSet tracks whether objects belong to a group — without preventing their collection.

const visited = new WeakSet();

function processNode(node) {
  if (visited.has(node)) return;  // already processed
  visited.add(node);

  // Process the node...
  node.children.forEach(processNode);
}

// When a node is GC'd, it's automatically removed from the WeakSet
// No risk of the Set growing unboundedly

Branding / type-checking:

const validatedInputs = new WeakSet();

function validate(input) {
  // ... validation logic ...
  if (isValid) {
    validatedInputs.add(input);
  }
  return isValid;
}

function submitForm(formData) {
  if (!validatedInputs.has(formData)) {
    throw new Error('Form data must be validated first');
  }
  // Safe to submit
}

WeakRef: Soft Caches and Nullable References

WeakMap and WeakSet are great when your keys are objects. But what if you want a reference to an object that doesn't prevent garbage collection, period? WeakRef gives you exactly that. You can check if the object is still alive by calling .deref():

let target = { data: 'important', payload: new ArrayBuffer(10_000_000) };
const weakRef = new WeakRef(target);

// Later...
const obj = weakRef.deref();
if (obj) {
  console.log(obj.data);  // object is still alive
} else {
  console.log('Object was garbage collected');
}

Building a WeakRef-Based Cache

class WeakCache {
  #cache = new Map();

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

    const value = ref.deref();
    if (!value) {
      // Object was collected — clean up the stale WeakRef
      this.#cache.delete(key);
      return undefined;
    }
    return value;
  }

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

const imageCache = new WeakCache();

function loadImage(url) {
  const cached = imageCache.get(url);
  if (cached) return cached;

  const img = createLargeImageObject(url);
  imageCache.set(url, img);
  return img;
}

// If memory pressure causes the GC to collect an image,
// the cache gracefully returns undefined and re-fetches
Common Trap

WeakRef.deref() may return the object even after it becomes unreachable — the GC hasn't run yet. And after a GC cycle, it may return undefined even though the object "should" still be alive (V8 decides when to collect). Never rely on precise timing of WeakRef invalidation. Treat it as: "the object might be alive." Design code that handles both cases.

Use WeakRef Sparingly

The TC39 proposal explicitly warns: "Correct use of WeakRef takes careful thought, and they are best avoided if possible." WeakRefs make program behavior depend on GC timing, which is non-deterministic. Use them for caches where a miss is acceptable, not for correctness-critical logic.

FinalizationRegistry: Cleanup Callbacks

Okay, this is the most exotic tool in the box. FinalizationRegistry lets you register a callback that fires when a registered object is garbage collected. It's the closest JavaScript gets to a destructor — and it comes with major caveats.

const registry = new FinalizationRegistry((heldValue) => {
  console.log(`Object associated with "${heldValue}" was collected`);
  // Perform cleanup: close connections, release external resources, etc.
});

function createResource(name) {
  const resource = { name, handle: openExternalHandle() };

  // When 'resource' is GC'd, the callback fires with 'name' as heldValue
  registry.register(resource, name);

  return resource;
}

let r = createResource('database-connection');
r = null;  // resource becomes eligible for GC
// Eventually: "Object associated with 'database-connection' was collected"

Practical FinalizationRegistry: External Resource Cleanup

const fileHandleRegistry = new FinalizationRegistry((handle) => {
  // Close the file handle when the wrapper object is GC'd
  handle.close();
  console.log(`Auto-closed file handle ${handle.id}`);
});

class ManagedFile {
  #handle;

  constructor(path) {
    this.#handle = openFile(path);
    // Register this instance — when GC'd, close the handle
    fileHandleRegistry.register(this, this.#handle);
  }

  read() {
    return this.#handle.readAll();
  }

  // Explicit close is still preferred — FinalizationRegistry is a safety net
  close() {
    this.#handle.close();
    fileHandleRegistry.unregister(this);  // no need for callback anymore
  }
}
FinalizationRegistry timing guarantees (or lack thereof)

The spec makes almost no timing guarantees:

  • The callback may fire at any point after the object becomes unreachable — could be milliseconds, seconds, or never (if the page closes first).
  • The callback runs as a microtask, so it won't interrupt synchronous code, but it may run between tasks.
  • V8 typically fires finalization callbacks during GC, but this is an implementation detail.
  • Multiple collected objects' callbacks may be batched.

This means FinalizationRegistry is a best-effort safety net, not a guaranteed cleanup mechanism. Always provide an explicit close() or dispose() method. Use FinalizationRegistry as a fallback for when users forget to call it.

WeakMap vs WeakRef: When to Use Which

With all these tools available, here's how to pick the right one.

ScenarioUse
Associate metadata with objects you don't ownWeakMap
Private fields (pre-#private syntax)WeakMap
Track whether objects belong to a setWeakSet
Cache computed results keyed by object identityWeakMap
Cache with string keys where values may be GC'dWeakRef (values are weak)
Build a soft-reference cache (evict under memory pressure)WeakRef + FinalizationRegistry
Clean up external resources when wrapper objects dieFinalizationRegistry
What developers doWhat they should do
Using Map when keys are objects that should be GC-eligible
Map keys are strong references that prevent GC, causing memory leaks for object-keyed caches
Use WeakMap — entries are automatically removed when keys are collected
Trying to iterate a WeakMap or check its size
Iteration would create strong references to all keys, defeating the weak reference purpose
WeakMap is intentionally non-iterable. If you need iteration, use Map with manual cleanup.
Relying on WeakRef.deref() timing for correctness
GC timing is non-deterministic. The object may survive longer than expected or be collected sooner.
Treat WeakRef.deref() as 'might be alive' — always handle the undefined case
Using FinalizationRegistry as the primary cleanup mechanism
Finalization callbacks have no timing guarantees — they may fire late or not at all before page unload
Provide explicit close/dispose methods. Use FinalizationRegistry as a safety net for forgotten cleanup.
Quiz
Why can't you iterate over a WeakMap's entries?
Quiz
What does weakRef.deref() return if the referenced object has been garbage collected?
Key Rules
  1. 1WeakMap keys are weak references — entries are auto-removed when the key object is GC'd. Use for metadata, caches, and private data keyed by object identity.
  2. 2WeakSet tracks membership without preventing GC. Use for visited/processed/validated tracking.
  3. 3WeakRef gives a non-preventing reference to any object. deref() returns the object or undefined. Use for soft caches where misses are acceptable.
  4. 4FinalizationRegistry fires a callback when a registered object is collected. No timing guarantees — use as a safety net, not primary cleanup.
  5. 5Always provide explicit cleanup methods (close, dispose, destroy). Weak references and finalization are fallbacks, not replacements.
  6. 6WeakMap and WeakSet cannot be iterated, sized, or cleared — this is by design, not a limitation.