DocsHub
ES6+

WeakMap and WeakSet

Learn how WeakMap and WeakSet work and when to use them for memory-efficient data storage.

WeakMap and WeakSet

WeakMap and WeakSet are the lesser-known siblings of Map and Set. They work similarly but with one fundamental difference — they hold weak references to their keys and values.

This means if the object you stored is no longer referenced anywhere else in your code, JavaScript's garbage collector can remove it automatically — even if it still exists in the WeakMap or WeakSet.


Understanding Weak References

First, a quick mental model of how memory works in JavaScript.

When you create an object and store it somewhere, JavaScript keeps it in memory as long as there is at least one reference to it. When nothing references it anymore, the garbage collector removes it.

let user = { name: "Ali" };
// user object is in memory — one reference

user = null;
// nothing references the object anymore
// garbage collector removes it

A regular Map holds a strong reference — it counts as a reference and keeps the object alive even if you set the original variable to null.

let user = { name: "Ali" };
const map = new Map();
map.set(user, "some data");

user = null;
// The object is NOT garbage collected
// map still holds a strong reference to it
// map.size is still 1

A WeakMap holds a weak reference — it does not count. If the object has no other references, it gets garbage collected regardless of being in the WeakMap.

let user = { name: "Ali" };
const weakMap = new WeakMap();
weakMap.set(user, "some data");

user = null;
// The object CAN now be garbage collected
// weakMap entry disappears automatically

WeakMap

A WeakMap is like a Map with three important restrictions:

  • Keys must be objects — not primitives
  • It is not iterable — no for...of, no .keys(), no .values()
  • No .size property

These restrictions exist because WeakMap does not know what it contains at any given moment — entries can disappear at any time as the garbage collector runs.

const weakMap = new WeakMap();

const user1 = { name: "Ali" };
const user2 = { name: "Sara" };

weakMap.set(user1, { lastLogin: "2024-03-15" });
weakMap.set(user2, { lastLogin: "2024-03-10" });

console.log(weakMap.get(user1)); // { lastLogin: "2024-03-15" }
console.log(weakMap.has(user2)); // true

weakMap.delete(user1);
console.log(weakMap.has(user1)); // false

WeakMap Methods

Only four methods — no iteration:

weakMap.set(key, value);   // add or update
weakMap.get(key);          // retrieve value
weakMap.has(key);          // check if key exists
weakMap.delete(key);       // remove entry

When to Use WeakMap

Storing private data for objects

Before private class fields (#field) existed, WeakMap was the standard way to attach private data to objects.

const _private = new WeakMap();

class User {
  constructor(name, password) {
    this.name = name;
    _private.set(this, { password, loginAttempts: 0 });
  }

  checkPassword(input) {
    const data = _private.get(this);
    if (input === data.password) {
      data.loginAttempts = 0;
      return true;
    }
    data.loginAttempts++;
    return false;
  }

  getLoginAttempts() {
    return _private.get(this).loginAttempts;
  }
}

const user = new User("Ali", "secret123");

console.log(user.checkPassword("wrong"));    // false
console.log(user.checkPassword("wrong"));    // false
console.log(user.getLoginAttempts());        // 2
console.log(user.checkPassword("secret123")); // true
console.log(user.getLoginAttempts());        // 0

// Private data is completely inaccessible from outside
console.log(user.password);      // undefined
console.log(_private.get(user)); // only works if you have access to _private

When the user object is garbage collected, its entry in _private disappears automatically — no memory leak.

Caching computed values

const cache = new WeakMap();

function processElement(element) {
  if (cache.has(element)) {
    console.log("Using cached result");
    return cache.get(element);
  }

  // Expensive computation
  const result = {
    width: element.offsetWidth,
    height: element.offsetHeight,
    area: element.offsetWidth * element.offsetHeight
  };

  cache.set(element, result);
  return result;
}

const btn = document.querySelector("button");
processElement(btn); // computes and caches
processElement(btn); // returns cached

// When btn is removed from the DOM and no longer referenced
// the cache entry disappears automatically — no manual cleanup needed

With a regular Map, you would need to manually delete the entry when the element is removed. With WeakMap, it happens automatically.

Tracking metadata without preventing cleanup

const metadata = new WeakMap();

function attachHandler(element, handler) {
  metadata.set(element, {
    handler,
    attachedAt: Date.now(),
    clickCount: 0
  });

  element.addEventListener("click", () => {
    const data = metadata.get(element);
    data.clickCount++;
    handler(data.clickCount);
  });
}

const btn = document.querySelector("button");

attachHandler(btn, (count) => {
  console.log(`Button clicked ${count} times`);
});

// When btn is removed from the DOM:
// - The WeakMap entry is garbage collected automatically
// - No memory leak from stale metadata

WeakSet

A WeakSet is like a Set with the same weak reference behavior:

  • Values must be objects — not primitives
  • Not iterable — no for...of, no .forEach()
  • No .size property
const weakSet = new WeakSet();

const user1 = { name: "Ali" };
const user2 = { name: "Sara" };

weakSet.add(user1);
weakSet.add(user2);

console.log(weakSet.has(user1)); // true
console.log(weakSet.has(user2)); // true

weakSet.delete(user1);
console.log(weakSet.has(user1)); // false

WeakSet Methods

Only three methods:

weakSet.add(value);    // add an object
weakSet.has(value);    // check if object exists
weakSet.delete(value); // remove object

When to Use WeakSet

Tracking objects without preventing garbage collection

The most common use — marking objects as "processed", "visited", or "seen" without keeping them alive.

const processedOrders = new WeakSet();

function processOrder(order) {
  if (processedOrders.has(order)) {
    console.log(`Order ${order.id} already processed — skipping.`);
    return;
  }

  // Process the order
  console.log(`Processing order ${order.id}...`);
  processedOrders.add(order);
}

const order1 = { id: 1, items: ["Laptop", "Mouse"] };
const order2 = { id: 2, items: ["Keyboard"] };

processOrder(order1); // Processing order 1...
processOrder(order1); // Order 1 already processed — skipping.
processOrder(order2); // Processing order 2...

Preventing circular processing

const visiting = new WeakSet();

function processNode(node) {
  if (visiting.has(node)) {
    return; // already visiting this node — circular reference
  }

  visiting.add(node);

  // Process node
  console.log(node.value);

  // Process children
  for (const child of node.children ?? []) {
    processNode(child);
  }

  visiting.delete(node);
}

Tracking DOM elements

const clickedElements = new WeakSet();

document.querySelectorAll("button").forEach(btn => {
  btn.addEventListener("click", () => {
    if (clickedElements.has(btn)) {
      console.log("Already clicked!");
      return;
    }

    clickedElements.add(btn);
    console.log("First click — doing something special");
    btn.classList.add("clicked");
  });
});

// When buttons are removed from the DOM
// they are automatically removed from clickedElements too

WeakMap vs WeakSet vs Map vs Set

// Map — strong reference, any key type, iterable, has .size
const map = new Map();
map.set("string key", value);     // ✅ string keys allowed
map.set(objectKey, value);        // ✅ object keys allowed
console.log(map.size);            // ✅ size available
for (const [k, v] of map) {}     // ✅ iterable

// WeakMap — weak reference, object keys only, not iterable
const weakMap = new WeakMap();
weakMap.set("string", value);     // ❌ only object keys
weakMap.set(objectKey, value);    // ✅
console.log(weakMap.size);        // ❌ undefined
for (const [k, v] of weakMap) {} // ❌ not iterable

// Set — strong reference, any value, iterable, has .size
const set = new Set();
set.add(42);                      // ✅ primitives allowed
set.add(object);                  // ✅ objects allowed
console.log(set.size);            // ✅
for (const v of set) {}          // ✅ iterable

// WeakSet — weak reference, objects only, not iterable
const weakSet = new WeakSet();
weakSet.add(42);                  // ❌ only objects
weakSet.add(object);              // ✅
console.log(weakSet.size);        // ❌ undefined
for (const v of weakSet) {}      // ❌ not iterable
MapWeakMapSetWeakSet
Key/value typesAnyObjects onlyAnyObjects only
ReferencesStrongWeakStrongWeak
Iterable
.size
Auto-cleanup
Use whenGeneral storageObject metadataUnique valuesObject tracking

When to Use Each

Need key-value storage?
  ├── Keys are objects AND want auto-cleanup → WeakMap
  └── Everything else → Map

Need unique values?
  ├── Values are objects AND want auto-cleanup → WeakSet
  └── Everything else → Set

WeakMap and WeakSet are niche tools — you will not reach for them often. But when you need to attach data to objects without preventing garbage collection, they are exactly the right tool. The most common real-world use is caching, private data storage, and tracking DOM elements.


Summary

  • WeakMap and WeakSet hold weak references — objects can be garbage collected even if they exist in them
  • Both only accept objects as keys or values — no primitives
  • Both are not iterable — no for...of, no .size, no .keys() or .values()
  • Entries disappear automatically when the object is garbage collected — no manual cleanup
  • WeakMap — attach metadata or cached data to objects without memory leaks
  • WeakSet — track which objects have been processed or visited without keeping them alive
  • Use Map and Set for general purpose storage — use WeakMap and WeakSet only when automatic cleanup from garbage collection is the specific requirement

On this page