JavaScript WeakSet: Weakly Held Object Collections

WeakSet is a specialized collection type in JavaScript that stores objects using weak references. Unlike a regular Set, objects in a WeakSet can be garbage collected when no other references to them...

Key Insights

  • WeakSet holds weak references to objects, allowing them to be garbage collected when no other references exist, preventing memory leaks in long-running applications
  • The API is intentionally limited (add, has, delete only) because entries can disappear at any time due to garbage collection, making enumeration impossible
  • Use WeakSet for tracking object metadata without preventing cleanup, like marking DOM nodes as processed or detecting circular references in algorithms

Introduction to WeakSet

WeakSet is a specialized collection type in JavaScript that stores objects using weak references. Unlike a regular Set, objects in a WeakSet can be garbage collected when no other references to them exist in your application. This makes WeakSet ideal for scenarios where you need to track objects without preventing them from being cleaned up by the garbage collector.

The syntax for creating and using a WeakSet is similar to Set, but the behavior is fundamentally different:

// Regular Set - holds strong references
const regularSet = new Set();
let obj1 = { id: 1, name: 'User' };
regularSet.add(obj1);

// WeakSet - holds weak references
const weakSet = new WeakSet();
let obj2 = { id: 2, name: 'Admin' };
weakSet.add(obj2);

// Both contain their respective objects
console.log(regularSet.has(obj1)); // true
console.log(weakSet.has(obj2));    // true

The critical difference becomes apparent when you remove all other references to an object. With a regular Set, the object remains in memory because the Set itself maintains a reference. With WeakSet, the object becomes eligible for garbage collection.

Core Characteristics and Limitations

WeakSet has several important constraints that stem from its weak reference behavior:

Only Objects Allowed: WeakSet can only store objects, not primitive values. This restriction exists because primitives are immutable and copied by value, making weak references meaningless.

Not Enumerable: You cannot iterate over a WeakSet or get its size. Since entries can be garbage collected at any time, enumeration would produce unreliable results.

No Size Property: For the same reason, there’s no way to know how many items are in a WeakSet.

Here’s what happens when you try to violate these constraints:

const weakSet = new WeakSet();

// This works - objects are allowed
const user = { name: 'Alice' };
weakSet.add(user);
console.log(weakSet.has(user)); // true

// These all throw TypeError
try {
    weakSet.add(42);          // Primitives not allowed
} catch (e) {
    console.log(e.message);   // Invalid value used in weak set
}

try {
    weakSet.add('string');    // Strings not allowed
} catch (e) {
    console.log(e.message);   // Invalid value used in weak set
}

try {
    weakSet.add(Symbol('id')); // Symbols not allowed
} catch (e) {
    console.log(e.message);   // Invalid value used in weak set
}

// No enumeration methods exist
console.log(weakSet.size);      // undefined
console.log(weakSet.forEach);   // undefined
console.log(weakSet.keys);      // undefined

WeakSet API Methods

WeakSet provides only three methods, and this minimalism is intentional:

add(object): Adds an object to the WeakSet
has(object): Checks if an object exists in the WeakSet
delete(object): Removes an object from the WeakSet

Methods like forEach(), clear(), or values() don’t exist because they would require enumerating entries that might disappear during execution.

const processedNodes = new WeakSet();

// Create some objects to track
const node1 = { type: 'div', id: 'header' };
const node2 = { type: 'span', id: 'title' };
const node3 = { type: 'p', id: 'content' };

// Add objects to track them
processedNodes.add(node1);
processedNodes.add(node2);

// Check if objects have been processed
console.log(processedNodes.has(node1)); // true
console.log(processedNodes.has(node3)); // false

// Adding the same object again has no effect
processedNodes.add(node1);
console.log(processedNodes.has(node1)); // still true

// Remove an object from tracking
processedNodes.delete(node1);
console.log(processedNodes.has(node1)); // false

// Delete returns true if object was present, false otherwise
console.log(processedNodes.delete(node2)); // true
console.log(processedNodes.delete(node2)); // false (already deleted)

Memory Management and Garbage Collection

The primary advantage of WeakSet is its interaction with JavaScript’s garbage collector. When an object in a WeakSet has no other references, it becomes eligible for garbage collection, and the WeakSet entry is automatically removed.

// Demonstrating garbage collection behavior (conceptual)
const trackedObjects = new WeakSet();

function createAndTrack() {
    let tempObject = { data: new Array(1000000).fill('x') };
    trackedObjects.add(tempObject);
    
    console.log(trackedObjects.has(tempObject)); // true
    
    // When this function ends, tempObject goes out of scope
    // If no other references exist, it becomes eligible for GC
    // The WeakSet entry will be automatically removed
}

createAndTrack();
// tempObject is now eligible for garbage collection
// The WeakSet won't prevent this cleanup

// Contrast with regular Set
const strongSet = new Set();

function createAndTrackStrong() {
    let tempObject = { data: new Array(1000000).fill('x') };
    strongSet.add(tempObject);
    
    // Even after this function ends, the object stays in memory
    // because strongSet maintains a reference
}

createAndTrackStrong();
// Object remains in memory - potential memory leak!

While you can’t directly observe garbage collection in JavaScript, understanding this behavior is crucial for preventing memory leaks in long-running applications.

Practical Use Cases

WeakSet excels in several real-world scenarios:

Tracking DOM Nodes Without Memory Leaks: Mark DOM elements as processed without preventing them from being garbage collected when removed from the document.

class DOMProcessor {
    constructor() {
        this.processedElements = new WeakSet();
    }
    
    process(element) {
        if (this.processedElements.has(element)) {
            console.log('Element already processed');
            return;
        }
        
        // Perform processing
        element.classList.add('processed');
        element.dataset.processedAt = Date.now();
        
        // Mark as processed
        this.processedElements.add(element);
    }
    
    isProcessed(element) {
        return this.processedElements.has(element);
    }
}

const processor = new DOMProcessor();
const button = document.querySelector('#myButton');

processor.process(button);
processor.process(button); // "Element already processed"

// When button is removed from DOM and no other references exist,
// it will be garbage collected along with its WeakSet entry

Object Tagging for Algorithms: Mark objects during traversal without modifying them or causing memory leaks.

function detectCircularReference(obj, visited = new WeakSet()) {
    // Check if we've seen this object before
    if (visited.has(obj)) {
        return true; // Circular reference detected
    }
    
    // Mark this object as visited
    visited.add(obj);
    
    // Check all properties
    for (let key in obj) {
        if (obj[key] && typeof obj[key] === 'object') {
            if (detectCircularReference(obj[key], visited)) {
                return true;
            }
        }
    }
    
    return false;
}

// Test with circular reference
const a = { name: 'A' };
const b = { name: 'B', ref: a };
a.ref = b; // Creates circular reference

console.log(detectCircularReference(a)); // true

WeakSet vs Set vs WeakMap

Choosing the right collection type depends on your requirements:

Use Set when: You need to store unique values (objects or primitives), iterate over entries, or check collection size.

Use WeakSet when: You need to tag objects without preventing garbage collection, and you only need to check membership.

Use WeakMap when: You need to associate metadata with objects without preventing garbage collection, and you need key-value pairs.

// Same use case: tracking processed items

// With Set - prevents garbage collection
const processedWithSet = new Set();
let item1 = { id: 1 };
processedWithSet.add(item1);
item1 = null; // Object still in memory due to Set

// With WeakSet - allows garbage collection
const processedWithWeakSet = new WeakSet();
let item2 = { id: 2 };
processedWithWeakSet.add(item2);
item2 = null; // Object can be garbage collected

// With WeakMap - when you need associated data
const metadataMap = new WeakMap();
let item3 = { id: 3 };
metadataMap.set(item3, { processedAt: Date.now(), status: 'complete' });
item3 = null; // Object and metadata can be garbage collected

Best Practices and Gotchas

Anti-Pattern: Expecting Enumeration

// WRONG - WeakSet is not enumerable
const weakSet = new WeakSet();
weakSet.add({ id: 1 });
weakSet.add({ id: 2 });

// These don't exist
// weakSet.forEach(item => console.log(item)); // ERROR
// console.log([...weakSet]); // ERROR
// console.log(weakSet.size); // undefined

Anti-Pattern: Using Primitives

// WRONG - only objects allowed
const weakSet = new WeakSet();
// weakSet.add(123); // TypeError
// weakSet.add('string'); // TypeError

// RIGHT - use objects
weakSet.add({ value: 123 });
weakSet.add({ value: 'string' });

Best Practice: Clear Semantic Purpose

// GOOD - WeakSet for membership tracking
class ObjectValidator {
    constructor() {
        this.validatedObjects = new WeakSet();
    }
    
    validate(obj) {
        // Validation logic
        if (obj && obj.requiredField) {
            this.validatedObjects.add(obj);
            return true;
        }
        return false;
    }
    
    isValid(obj) {
        return this.validatedObjects.has(obj);
    }
}

Debugging Tip: Since you can’t inspect WeakSet contents, maintain a parallel Set during development for debugging, but remove it in production.

class DebugTracker {
    constructor(debug = false) {
        this.weakSet = new WeakSet();
        this.debugSet = debug ? new Set() : null;
    }
    
    add(obj) {
        this.weakSet.add(obj);
        if (this.debugSet) this.debugSet.add(obj);
    }
    
    has(obj) {
        return this.weakSet.has(obj);
    }
    
    debugContents() {
        if (this.debugSet) {
            console.log('Current items:', [...this.debugSet]);
        }
    }
}

WeakSet is a specialized tool that solves specific problems around memory management and object tracking. Use it when you need to mark objects without preventing garbage collection, and you’ll avoid entire classes of memory leaks in your applications.

Liked this? There's more.

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