Back to JavaScript and TypeScript

WeakMap, WeakSet, and WeakRef

WeakMap, WeakSet, and WeakRef hold references to objects without preventing garbage collection. They're the right tool when you want to attach metadata to objects or track membership without causing memory leaks.

WeakMap

WeakMap

Keys must be objects. When a key object is no longer reachable from anywhere else, the entry is automatically removed. You cannot iterate a WeakMap or read its size.

WeakMap keyed by object, GC-friendly

A WeakMap only accepts objects as keys, and holds them *weakly* when no other reference to the key object exists, the entry is automatically removed by the garbage collector. This prevents the WeakMap from becoming a memory leak.

const cache = new WeakMap()

function processUser(user) {
  if (cache.has(user)) {
    return cache.get(user)  // reuse cached result
  }
  const result = expensiveCompute(user)
  cache.set(user, result)
  return result
}

let user = { id: 1, name: "Alice" }
processUser(user)

// When user is no longer referenced, the WeakMap entry
// is automatically eligible for garbage collection:
user = null  // GC can now clean up the entry

WeakMap vs Map the key difference

A regular Map holds a strong reference to its keys. Even if you set user = null, the Map keeps the object alive. A WeakMap does not prevent GC the entry disappears when the key is no longer reachable.

// Map  strong reference, potential memory leak
const strongMap = new Map()
let obj = { data: "big" }
strongMap.set(obj, "value")
obj = null
// obj's data is still alive  strongMap holds it

// WeakMap  weak reference, GC safe
const weakMap = new WeakMap()
let obj2 = { data: "big" }
weakMap.set(obj2, "value")
obj2 = null
// obj2 can now be garbage collected

// WeakMap is not iterable  you can't list all entries
// weakMap.keys()   → TypeError
// weakMap.size     → undefined

Private data per instance (before #fields)

Before private class fields (#field), WeakMap was the standard way to store truly private per-instance data. It's still useful when you can't modify the class.

const _balance = new WeakMap()

class BankAccount {
  constructor(initial) {
    _balance.set(this, initial)
  }
  deposit(amount) {
    _balance.set(this, _balance.get(this) + amount)
  }
  get balance() {
    return _balance.get(this)
  }
}

const acc = new BankAccount(100)
acc.deposit(50)
console.log(acc.balance)   // 150
console.log(acc._balance)  // undefined  truly private

WeakSet

WeakSet

Like Set, but stores objects weakly. Great for marking objects without keeping them alive.

WeakSet tracking objects without retaining them

WeakSet stores objects weakly. It's useful for tracking membership e.g. marking objects as 'seen' or 'processed' without preventing those objects from being garbage collected.

const visited = new WeakSet()

function processOnce(node) {
  if (visited.has(node)) return
  visited.add(node)
  // process node...
  node.children?.forEach(processOnce)
}

// Once nodes are removed from the tree and no other refs exist,
// WeakSet entries are automatically cleaned up

WeakRef

WeakRef and FinalizationRegistry

WeakRef gives you an explicit nullable handle to an object. Its companion FinalizationRegistry can run cleanup code after GC but GC timing is non-deterministic, so these are for best-effort cleanup only.

WeakRef an explicit weak reference

WeakRef wraps an object and holds it weakly. Call .deref() to get the object it returns undefined if GC has already collected it. Always check for undefined before use.

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

// Some time passes...
const obj = ref.deref()
if (obj) {
  console.log(obj.data)  // "important"  (if not yet GC'd)
} else {
  console.log("Object was garbage collected")
}

WeakRef + FinalizationRegistry cleanup callbacks

FinalizationRegistry lets you register a callback that fires *after* an object is garbage collected. Use this to clean up associated resources. Note: GC timing is non-deterministic don't rely on it for critical cleanup.

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

let resource = { id: "conn-1", socket: "..." }

// Register: when resource is GC'd, log its id
registry.register(resource, resource.id)

resource = null  // GC-eligible; eventually logs "Cleaned up: conn-1"

Reference

When to Use Each

A quick comparison of all five collection types and when to reach for each one.

TypeKeys / ValuesIterableBest For
WeakMapObjects onlyNoPer-object metadata, private data
WeakSetObjects onlyNoMembership tracking (visited, processed)
WeakRefN/ANoOptional reference; check deref() before use
MapAny valueYesGeneral key-value store with full API
SetAny valueYesUnique value collection with full API