Skip to content

WeakRef, WeakMap, and WeakSet

intermediate12 min read

References That Don't Prevent Garbage Collection

Here's a scenario you've probably dealt with: you want to attach some metadata to a DOM element — maybe a click count, or a timestamp. You could use a Map, but what happens when that element gets removed from the page? If your Map still holds a reference to it, the element (and your metadata) stay in memory forever. That's a memory leak.

WeakMap, WeakSet, and WeakRef solve this problem. They hold references to objects that don't count for garbage collection. If the object is no longer reachable through any other path, the garbage collector is free to collect it — even though a WeakMap/WeakSet/WeakRef still points to it.

Mental Model

Normal references are like handcuffs — as long as you're holding on, the object can't leave. Weak references are like sticky notes — you've written a reference to the object, but if the object leaves (gets garbage collected), the sticky note just goes blank. You held onto the note, but not the object.

WeakMap — The Right Way to Attach Metadata

Let's start with the one you'll use most often. A WeakMap is a key-value store where keys must be objects (or non-registered symbols), and keys are held weakly:

const metadata = new WeakMap();

function processElement(element) {
  // Attach metadata to a DOM element without modifying it
  metadata.set(element, {
    processedAt: Date.now(),
    clickCount: 0
  });
}

function handleClick(element) {
  const data = metadata.get(element);
  if (data) data.clickCount++;
}

// When the DOM element is removed and no other references exist,
// the WeakMap entry is automatically garbage collected.
// No manual cleanup needed. No memory leak.

WeakMap vs Map — The Critical Difference

// Map holds STRONG references to keys
const cache = new Map();
function processWithMap(obj) {
  cache.set(obj, expensiveComputation(obj));
}
// Even after all other references to obj are gone,
// the Map keeps it alive → MEMORY LEAK

// WeakMap holds WEAK references to keys
const cache = new WeakMap();
function processWithWeakMap(obj) {
  cache.set(obj, expensiveComputation(obj));
}
// When obj is no longer referenced elsewhere,
// both the key and value are garbage collected

Why WeakMap Keys Must Be Objects

This restriction surprises people, but it makes perfect sense once you think about it. Primitives (numbers, strings, booleans) are not garbage collected the way objects are — they're managed by value, not by reference. There's no way to "weakly reference" the number 42 because 42 doesn't have a unique identity in memory.

const wm = new WeakMap();
wm.set(42, "data");        // TypeError: Invalid value used as weak map key
wm.set("hello", "data");   // TypeError
wm.set({}, "data");         // OK — objects have unique identity
wm.set(Symbol("x"), "data"); // OK in modern engines (non-registered symbols)

WeakMap Is Not Enumerable

You cannot iterate over a WeakMap. No .keys(), .values(), .entries(), .forEach(), no .size:

const wm = new WeakMap();
wm.set({}, "a");
wm.size;      // undefined
[...wm];      // TypeError: wm is not iterable
wm.keys();    // TypeError: wm.keys is not a function
Common Trap

This isn't a limitation — it's a design requirement. If WeakMaps were enumerable, you could observe when entries disappear due to garbage collection. But garbage collection is non-deterministic — it can happen at any time. Making it observable would create race conditions and make program behavior depend on GC timing, which would be a nightmare. The non-enumerability guarantees that GC timing never affects program logic.

WeakMap Use Cases

Private Data (Pre-#private fields)

const _private = new WeakMap();

class Person {
  constructor(name, ssn) {
    this.name = name;
    _private.set(this, { ssn }); // Private data attached externally
  }

  getSSN(authorized) {
    if (!authorized) throw new Error("Unauthorized");
    return _private.get(this).ssn;
  }
}

const p = new Person("Alice", "123-45-6789");
p.name;           // "Alice" — public
p.ssn;            // undefined — not on the object
_private.get(p);  // { ssn: "123-45-6789" } — only accessible through the WeakMap
// When p is garbage collected, the private data is too

DOM Node Metadata

const nodeData = new WeakMap();

function trackElement(element) {
  nodeData.set(element, {
    impressionTime: Date.now(),
    viewed: false,
    interactions: 0
  });
}

// When the element is removed from the DOM and dereferenced,
// the metadata is automatically cleaned up. Zero manual bookkeeping.

Memoization Without Memory Leaks

const cache = new WeakMap();

function expensiveOperation(obj) {
  if (cache.has(obj)) return cache.get(obj);

  const result = /* heavy computation based on obj */;
  cache.set(obj, result);
  return result;
}

// Cache entries for unreachable objects are automatically cleaned up

WeakSet — Object Tagging

WeakSet is like Set but with weak references and no enumeration. It's used for marking or tagging objects:

const visited = new WeakSet();

function processOnce(obj) {
  if (visited.has(obj)) return; // Already processed
  visited.add(obj);
  // Do expensive processing...
}

// No need to clean up visited — entries for unreachable objects
// are automatically garbage collected

Circular Reference Detection

function deepClone(obj, seen = new WeakSet()) {
  if (obj === null || typeof obj !== "object") return obj;
  if (seen.has(obj)) throw new Error("Circular reference detected");

  seen.add(obj);
  const clone = Array.isArray(obj) ? [] : {};
  for (const key of Reflect.ownKeys(obj)) {
    clone[key] = deepClone(obj[key], seen);
  }
  return clone;
}

WeakRef — Soft References

Now we're getting into the advanced territory. WeakRef provides a weak reference to a single object, with the ability to check if it's still alive:

let target = { data: "important" };
const ref = new WeakRef(target);

ref.deref(); // { data: "important" } — still alive

target = null; // Remove the strong reference
// ... at some point after GC runs ...
ref.deref(); // undefined — the object was collected
WeakRef is a last resort

The spec explicitly warns: "Correct use of WeakRef requires careful thought, and they are best avoided if possible." The behavior depends on garbage collection timing, which is non-deterministic and varies between engines, versions, and heap pressure. Never use WeakRef in logic that requires the object to be alive or dead at a specific time.

WeakRef Cache Pattern

class Cache {
  #map = new Map();

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

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

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

FinalizationRegistry — Cleanup Callbacks

This one's niche, but when you need it, nothing else will do. FinalizationRegistry lets you register a callback that runs when an object is garbage collected:

const registry = new FinalizationRegistry((heldValue) => {
  console.log(`Object associated with "${heldValue}" was collected`);
  // Clean up external resources: close file handles, deregister from server, etc.
});

let obj = { name: "Alice" };
registry.register(obj, "alice-data"); // "alice-data" is the held value

obj = null;
// Eventually, after GC: logs "Object associated with 'alice-data' was collected"
FinalizationRegistry for resource cleanup

The primary use case is cleaning up external resources that JavaScript's GC doesn't know about:

const fileRegistry = new FinalizationRegistry((fd) => {
  // Close the file descriptor when the JS wrapper is collected
  closeFileDescriptor(fd);
});

class FileHandle {
  #fd;
  constructor(path) {
    this.#fd = openFile(path);
    fileRegistry.register(this, this.#fd);
  }
  read() { return readFile(this.#fd); }
  close() {
    closeFileDescriptor(this.#fd);
    fileRegistry.unregister(this); // Prevent double-close
  }
}

But don't rely on this for critical cleanup — the spec doesn't guarantee the callback will ever run (the program might exit first). Always provide an explicit close() method too.

Execution Trace
Create
const wm = new WeakMap()
Empty WeakMap created
Set
wm.set(obj, data)
Key (obj) held weakly. Value (data) held strongly.
GC eligible
obj = null (no more strong refs)
obj becomes eligible for garbage collection
GC runs
Both key and value removed from WeakMap
Timing is non-deterministic — could be immediate or much later
Access
wm.get(obj) — but obj is null
No way to access the entry anymore. It's gone.

Production Scenario: Preventing Event Listener Leaks

const listenerRegistry = new WeakMap();

function safeAddEventListener(element, event, handler) {
  // Track listeners per element
  if (!listenerRegistry.has(element)) {
    listenerRegistry.set(element, []);
  }
  listenerRegistry.get(element).push({ event, handler });
  element.addEventListener(event, handler);
}

function removeAllListeners(element) {
  const listeners = listenerRegistry.get(element) || [];
  for (const { event, handler } of listeners) {
    element.removeEventListener(event, handler);
  }
  listenerRegistry.delete(element);
}

// When element is removed from the DOM and dereferenced,
// the WeakMap entry (including the listener list) is cleaned up.
// No manual cleanup needed for the WeakMap itself.
What developers doWhat they should do
Using Map when keys are objects that should be garbage collected
Map holds strong references to keys, preventing garbage collection and causing memory leaks
Use WeakMap — entries are automatically cleaned when keys are unreachable
Trying to iterate over WeakMap/WeakSet
Enumerability would expose GC timing, creating non-deterministic program behavior
WeakMap/WeakSet are not enumerable by design — use Map/Set if you need iteration
Using WeakRef in logic that depends on the object being alive
GC timing is non-deterministic. WeakRef objects can be collected at any time.
Always handle the case where deref() returns undefined. Use strong references when the object must be alive.
Using primitive keys with WeakMap
Primitives don't have unique identity — they're compared by value, not reference
WeakMap keys must be objects (or non-registered symbols). Use Map for primitive keys.
Quiz
Why can't you enumerate the entries of a WeakMap?
Quiz
What happens to a WeakMap's VALUE when its KEY is garbage collected?
Quiz
What is the held value parameter in FinalizationRegistry.register(target, heldValue)?
Key Rules
  1. 1WeakMap/WeakSet hold weak references — entries are automatically cleaned when keys become unreachable
  2. 2WeakMap keys must be objects or non-registered symbols. Values are held strongly.
  3. 3WeakMap/WeakSet are not enumerable — this is by design to keep GC timing unobservable
  4. 4WeakRef.deref() can return undefined at any time — always handle both cases
  5. 5FinalizationRegistry callbacks are not guaranteed to run — always provide explicit cleanup methods too