JavaScript has Map, which stores key‑value pairs, and a similarly named WeakMap. Both can be registered with set() and retrieved with get(). However, WeakMap does not have size nor keys().

A WeakMap is a mechanism that associates information with objects without keeping the objects in memory solely because of that association. The term “weak” refers not to the data’s reliability or the accuracy of lookups, but to how the references to the keys are treated.

First, let’s look at it from the user’s perspective. The internal handling by the garbage collector and the actual bugs will be covered in the next section, “WeakMap and Generational GC” (source).

Attaching information to objects

For example, suppose you want to record how many times an object has been used without adding a property to the object itself.

const counts = new WeakMap();
const item = { name: "button" };

counts.set(item, 0);
counts.set(item, counts.get(item) + 1);

console.log(counts.get(item)); // 1
console.log(item); // { name: "button" }

Using item as the key, the count is stored elsewhere. Because the object itself isn’t modified, you avoid name collisions with existing properties. Even a frozen object can have information associated with it from the outside.

const item = Object.freeze({ name: "button" });
const descriptions = new WeakMap();

descriptions.set(item, "the submit button on the screen");
console.log(descriptions.get(item)); // the submit button on the screen

Here, the frozen object is item. Registering it in the WeakMap does not modify the property of item.

The basic operations are set(), get(), has(), and delete(). For a get() key that has not been registered, undefined is returned. You can also store undefined as a value, and in that case you use has() to determine whether a registration exists.

const data = new WeakMap();
const key = {};

data.set(key, undefined);
console.log(data.get(key)); // undefined
console.log(data.has(key)); // true

console.log(data.delete(key)); // true
console.log(data.has(key)); // false

The behavior of these methods is described in the ECMAScript specification for WeakMap (source).

Different keys for the same content

A WeakMap does not search by comparing the contents of objects.

const data = new WeakMap();
const a = { id: 1 };
const b = { id: 1 };

data.set(a, "registered");

console.log(data.get(a)); // registered
console.log(data.get(b)); // undefined
console.log(data.get({ id: 1 })); // undefined

a and b both contain the same data, id: 1, but they are distinct objects. Even if you create a copy, it does not become the original key. Conversely, even if you modify the properties of an object, the entry registered with that object as the key can still be retrieved.

If you want to search by numeric IDs or string names, using a Map is more natural. When you use a string or number as a key in a WeakMap, it becomes set(), which results in TypeError.

Under the current specification, in addition to objects, Symbols that are not globally registered with Symbol.for() can also be used as keys. Symbol("id") is allowed, but Symbol.for("id") is not. This distinction is defined by the CanBeHeldWeakly abstract operation (source). In the examples of this article, we use objects whose purpose is easy to understand.

References retained in a Map

You can also register objects as keys in a normal Map.

const data = new Map();
let item = { name: "button" };

data.set(item, { clicks: 0 });
item = null;

console.log(data.size); // 1

item = null is an operation that removes the reference from the original object held by the variable item. It does not delete the object itself. Since the Map still retains the key, both the key and its value remain as long as the Map is reachable from the program.

flowchart LR
    R[Map usable from the program] --> M[Entry in the map]
    M --> K[Key object]
    M --> V[Value]

The garbage collector follows references from locations the program can still reach to determine which objects are still needed. The starting point of this traversal is called a root. Even if you break one reference to a variable, if that variable can still be reached via another path from a root, it will not be collected.

In this Map example, you can also retrieve the key using keys(). Even if the variable item becomes unreachable, the entry remains accessible through the Map.

If you store auxiliary information for objects in a long‑lived Map and never delete the entries that are no longer needed, the Map will continue to retain the keys. In scenarios where UI components are repeatedly recreated, components that are no longer used can persist via the auxiliary‑information Map.

Registration alone does not keep entries in a WeakMap

When the same association is made with a WeakMap, the WeakMap does not retain the key solely because it is registered.

const data = new WeakMap();
let item = { name: "button" };

data.set(item, { clicks: 0 });
item = null;

If there are no other references to the key besides this code, the key becomes eligible for garbage collection. You don’t need to explicitly remove the key from the WeakMap with delete() once it’s no longer needed. You can attach auxiliary information to an object for the duration of its lifetime. The MDN documentation on WeakMap also explains its use case for associating information with objects from the outside (documentation).

However, putting something into a WeakMap does not guarantee that memory will be freed quickly. If other arrays, variables, or pending operations still hold a reference to the key, the key remains usable. The timing of when the garbage collector actually runs is also not determined by this JavaScript code.

Moreover, if you retrieve a value from a WeakMap and store it elsewhere, that value acquires a new reference. Even if the WeakMap entry becomes unnecessary, the value that is still being used outside the WeakMap does not disappear.

You cannot enumerate keys

A WeakMap lacks keys(), values(), entries(), and size. It also does not provide a way to enumerate its entries with for...of.

If enumeration were possible, one could observe whether keys that have become unused are still retained by the garbage collector or have already been collected. The program’s behavior would then depend on when the garbage collector runs. A WeakMap provides an API that lets you pass a key you already have and look up its associated value. The specification’s explanation (source) also cites this point as the reason for not providing enumeration.

To call has(key), the caller must already possess that key. This is the distinction from enumeration: you cannot discover keys known only to the WeakMap from the outside.

If you need to display the number of entries, process all registrations, or discard entries in order from the oldest, you should use a Map and manage the deletion criteria yourself. Storing the keys placed in a WeakMap in another array to create a list will cause that array to hold strong references to the keys.

Per‑object computed results

You can also use a WeakMap to cache the results of a computation that takes an object as input, reusing the result only while the object remains in use.

const labels = new WeakMap();

function labelFor(user) {
  if (labels.has(user)) {
    return labels.get(user);
  }

  const label = `${user.familyName} ${user.givenName}`;
  labels.set(user, label);
  return label;
}

If you pass the same user again, the cached result is returned. A user that is no longer used anywhere will not be kept solely because it is registered in the cache.

On the other hand, if you modify user.familyName, the WeakMap will not automatically recompute the result. Since the key remains the same object, it will return the old result. You must either design the input to be immutable, or invalidate the entry with labels.delete(user) when the input changes.

A WeakMap can tie the lifetime of a cache entry to its key, but it does not manage the correctness of the cached results or enforce an upper bound on memory usage. In programs that keep many keys alive for a long time, the corresponding entries will remain as well.

When the value references the key

In the previous examples, the values were numbers or strings. You can also store an object as the value and have that object reference the key.

const data = new WeakMap();
let item = { name: "button" };

data.set(item, { owner: item });
item = null;

If a WeakMap were to retain the value unconditionally, the value’s owner would allow you to reach the key again. In that case, the entry itself would keep the key alive even after it is no longer needed.

Therefore, the garbage collector that implements WeakMap’s reference semantics must treat the value conditionally: it may follow the value’s reference only when the key is still reachable. This relationship is represented by an ephemeron. The Racket documentation on ephemerons (source) also describes how to handle cases where the value contains a reference back to the key.

Simply having a reference through the value cannot satisfy the entry’s own liveness condition. Conversely, if the value continues to be used by another variable, you can reach the key via that variable through owner. In this case, the reason the key remains is not because it was registered in a WeakMap, but due to an ordinary external reference.

This explains the liveness relationships as determined by garbage collection. It is not an example that demonstrates that an object that becomes unnecessary is always immediately reclaimed.

The subsequent article “WeakMap and Generational GC” (source) discusses the order in which these conditional references are resolved and the mechanism for recording references from older objects to younger ones. Using Boa, which is integrated into Omoikane, we will examine the fix that prevented reachable objects from being incorrectly reclaimed, looking at both the implementation and the regression tests.