JavaScript WeakMap: Weak References for Objects

WeakMap is JavaScript's specialized collection type for storing key-value pairs where keys are objects and the references to those keys are 'weak.' This means if an object used as a WeakMap key has...

Key Insights

  • WeakMap holds weak references to object keys, allowing automatic garbage collection when objects are no longer referenced elsewhere—unlike regular Map which prevents garbage collection of its keys
  • WeakMap is non-iterable and lacks size/enumeration methods by design, making it ideal for private data storage and metadata tracking without memory leaks
  • Use WeakMap for associating metadata with objects you don’t own (like DOM nodes) or implementing private class properties, but choose Map when you need iteration, serialization, or primitive keys

Introduction to WeakMap

WeakMap is JavaScript’s specialized collection type for storing key-value pairs where keys are objects and the references to those keys are “weak.” This means if an object used as a WeakMap key has no other references in your program, the garbage collector can reclaim that object’s memory—even though it’s still technically a key in the WeakMap.

This is fundamentally different from a regular Map, which holds strong references to its keys and prevents them from being garbage collected. That difference makes WeakMap invaluable for specific scenarios where you need to associate data with objects without accidentally creating memory leaks.

// Regular Map - holds strong reference
const regularMap = new Map();
let obj = { id: 1 };
regularMap.set(obj, 'some data');
obj = null; // Object still can't be garbage collected

// WeakMap - holds weak reference
const weakMap = new WeakMap();
let obj2 = { id: 2 };
weakMap.set(obj2, 'some data');
obj2 = null; // Object can now be garbage collected

The syntax looks similar, but the memory management implications are profound.

WeakMap Fundamentals

WeakMap provides a minimal API with just four methods: set(), get(), has(), and delete(). This simplicity is intentional—WeakMap sacrifices features for its memory management guarantees.

const wm = new WeakMap();

// Creating object keys
const user = { name: 'Alice' };
const config = { theme: 'dark' };

// set(key, value) - add or update entries
wm.set(user, { loginCount: 5, lastLogin: new Date() });
wm.set(config, { version: '2.1' });

// get(key) - retrieve values
console.log(wm.get(user)); // { loginCount: 5, lastLogin: ... }

// has(key) - check existence
console.log(wm.has(user)); // true
console.log(wm.has({})); // false (different object reference)

// delete(key) - remove entries
wm.delete(config);
console.log(wm.has(config)); // false

The critical restriction: keys must be objects. Primitives aren’t allowed because they’re immutable and don’t have a lifecycle that garbage collection can track.

const wm = new WeakMap();

// These all throw TypeError
try {
  wm.set('string-key', 'value');
} catch (e) {
  console.log(e.message); // Invalid value used as weak map key
}

try {
  wm.set(42, 'value');
} catch (e) {
  console.log(e.message); // Invalid value used as weak map key
}

// Only objects work
wm.set({}, 'value'); // ✓ Works
wm.set([], 'value'); // ✓ Works
wm.set(function() {}, 'value'); // ✓ Works

Memory Management & Garbage Collection

The “weak” in WeakMap refers to weak references in memory management. When you store an object in a regular Map, that Map holds a strong reference—the object cannot be garbage collected even if nothing else in your program references it.

WeakMap’s weak references allow the garbage collector to reclaim objects when they’re no longer needed elsewhere. This prevents a common memory leak pattern where metadata or caching structures unintentionally keep objects alive indefinitely.

// Demonstrating the concept (actual GC timing varies by engine)

// Strong reference with Map
function memoryLeakExample() {
  const cache = new Map();
  
  for (let i = 0; i < 1000; i++) {
    const largeObject = {
      data: new Array(10000).fill(i),
      id: i
    };
    cache.set(largeObject, `processed-${i}`);
  }
  
  // Even after this function ends, all 1000 objects
  // remain in memory because cache holds strong references
  return cache;
}

// Weak reference with WeakMap
function memorySafeExample() {
  const cache = new WeakMap();
  
  for (let i = 0; i < 1000; i++) {
    const largeObject = {
      data: new Array(10000).fill(i),
      id: i
    };
    cache.set(largeObject, `processed-${i}`);
  }
  
  // Objects can be garbage collected because WeakMap
  // doesn't prevent it (assuming no other references exist)
  return cache;
}

This automatic cleanup is WeakMap’s superpower. You get a data structure that cleans itself up as your program runs.

Practical Use Cases

Private Data Storage

Before private class fields (#field), WeakMap was the cleanest way to implement truly private data:

const privateData = new WeakMap();

class BankAccount {
  constructor(balance) {
    privateData.set(this, { balance });
  }
  
  deposit(amount) {
    const data = privateData.get(this);
    data.balance += amount;
  }
  
  getBalance() {
    return privateData.get(this).balance;
  }
}

const account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
console.log(account.balance); // undefined - truly private!

When the account instance is garbage collected, its private data automatically disappears too.

Self-Cleaning Cache

WeakMap excels at caching computed values for objects without creating memory leaks:

const computationCache = new WeakMap();

function expensiveOperation(obj) {
  // Check cache first
  if (computationCache.has(obj)) {
    console.log('Cache hit!');
    return computationCache.get(obj);
  }
  
  // Perform expensive computation
  console.log('Computing...');
  const result = {
    processed: JSON.stringify(obj).length,
    timestamp: Date.now()
  };
  
  // Cache the result
  computationCache.set(obj, result);
  return result;
}

let data = { name: 'Test', values: [1, 2, 3, 4, 5] };
expensiveOperation(data); // Computing...
expensiveOperation(data); // Cache hit!

data = null; // Cache entry automatically cleaned up

DOM Node Metadata

WeakMap is perfect for tracking metadata about DOM elements without preventing their cleanup:

const elementMetadata = new WeakMap();

function trackElement(element, data) {
  elementMetadata.set(element, {
    ...data,
    createdAt: Date.now()
  });
}

function getElementData(element) {
  return elementMetadata.get(element);
}

// Track some DOM elements
const button = document.createElement('button');
trackElement(button, { clicks: 0, type: 'submit' });

button.addEventListener('click', () => {
  const data = getElementData(button);
  data.clicks++;
});

// When button is removed from DOM and no references remain,
// metadata is automatically garbage collected

WeakMap Limitations & When Not to Use

WeakMap’s design trades features for memory safety. It’s not iterable—no forEach(), no keys(), no values(), no entries(), and no size property.

const wm = new WeakMap();
wm.set({}, 'value1');
wm.set({}, 'value2');

console.log(wm.size); // undefined
console.log([...wm]); // TypeError: wm is not iterable

// You cannot:
// - Loop through entries
// - Get a count of entries
// - Serialize to JSON
// - List all keys or values

This isn’t a bug—it’s fundamental to WeakMap’s design. Since entries can be garbage collected at any time, enumerating them would be unreliable and could prevent garbage collection.

Use Map instead when you need:

// Iteration over entries
const userScores = new Map();
userScores.set('alice', 100);
userScores.set('bob', 85);
for (const [user, score] of userScores) {
  console.log(`${user}: ${score}`);
}

// Primitive keys
const statusCodes = new Map();
statusCodes.set(200, 'OK');
statusCodes.set(404, 'Not Found');

// Serialization
const config = new Map([['theme', 'dark'], ['lang', 'en']]);
const json = JSON.stringify([...config]);

// Size tracking
console.log(`Total users: ${userScores.size}`);

Performance Considerations

WeakMap operations (get, set, has, delete) are typically O(1), similar to Map. The performance advantage comes from memory efficiency, not speed.

// Benchmark: WeakMap vs Map for object metadata
function benchmarkMetadataStorage(iterations) {
  const objects = Array.from({ length: iterations }, () => ({}));
  
  // Map approach
  console.time('Map');
  const map = new Map();
  objects.forEach(obj => map.set(obj, { data: 'test' }));
  console.timeEnd('Map');
  
  // WeakMap approach
  console.time('WeakMap');
  const weakMap = new WeakMap();
  objects.forEach(obj => weakMap.set(obj, { data: 'test' }));
  console.timeEnd('WeakMap');
  
  // Memory consideration: Map keeps all objects alive
  // WeakMap allows them to be collected when objects array is cleared
}

benchmarkMetadataStorage(100000);

The real performance benefit appears in long-running applications where WeakMap prevents memory bloat by allowing automatic cleanup.

Conclusion

WeakMap is a specialized tool for a specific problem: associating data with objects without creating memory leaks. Its weak references enable automatic cleanup, making it ideal for private data, caching, and metadata tracking.

Choose WeakMap when you need to attach data to objects you don’t control (DOM nodes, third-party objects) or when implementing patterns like private properties. The automatic memory management prevents the subtle leaks that plague Map-based solutions.

However, don’t reach for WeakMap by default. Use Map when you need iteration, primitive keys, serialization, or size tracking. WeakMap’s limitations are intentional—the price of its memory management guarantees.

The decision criteria is straightforward: if you’re storing object metadata that should disappear when the object does, use WeakMap. For everything else, Map is likely the better choice.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.