How to Use WeakMap and WeakSet in JavaScript
Map and Set are powerful data structures, but they have a characteristic that can cause problems in long-running applications: they keep strong references to their keys and values, preventing the garbage collector from reclaiming memory even when those keys or values are no longer needed anywhere else in your program.
WeakMap and WeakSet solve this problem by holding weak references to their contents. When the only remaining reference to an object is inside a WeakMap or WeakSet, the garbage collector is free to reclaim that object's memory. The entry simply disappears from the collection automatically, without any manual cleanup.
This behavior makes WeakMap and WeakSet ideal for scenarios where you need to associate extra data with objects (caching, metadata, private properties, tracking) without preventing those objects from being garbage collected when they are no longer used. This guide explains the memory problem they solve, how they work, their practical use cases, and the trade-offs that come with their "weak" nature.
The Problem: Memory Leaks with Map/Set
To understand why WeakMap and WeakSet exist, you first need to understand the memory problem with regular Map and Set.
Strong References Keep Objects Alive
When you use an object as a key in a Map or add an object to a Set, the collection holds a strong reference to that object. As long as the Map or Set exists, the object cannot be garbage collected, even if every other reference to it has been removed.
let map = new Map();
let user = { name: "Alice", data: new Array(1000000).fill("x") };
// Map holds a strong reference to user
map.set(user, "some metadata");
// Remove our only other reference to the object
user = null;
// The object is STILL alive in memory!
// The Map's reference prevents garbage collection
console.log(map.size); // 1 (the entry persists)
// The only way to free the memory is to manually delete it
// map.delete(user); but we can't, because we lost the reference!
This is a memory leak. The large object with a million-element array stays in memory forever because the Map holds onto it, even though no part of your application needs it anymore.
A Real-World Scenario
Consider a web application that tracks metadata for DOM elements:
let elementMetadata = new Map();
function trackElement(element) {
elementMetadata.set(element, {
createdAt: Date.now(),
clickCount: 0,
lastInteraction: null
});
}
function onElementClick(element) {
let meta = elementMetadata.get(element);
if (meta) {
meta.clickCount++;
meta.lastInteraction = Date.now();
}
}
// Track some elements
let button = document.createElement("button");
trackElement(button);
// Later, the button is removed from the DOM
button.remove();
button = null;
// The DOM element is gone from the page and from our variable,
// but elementMetadata STILL holds a reference to it!
// The element and its metadata are leaked.
console.log(elementMetadata.size); // Still 1
In a single-page application where elements are frequently created and destroyed, this pattern causes memory to grow continuously. You would need to manually call elementMetadata.delete(element) every time an element is removed, which is error-prone and creates tight coupling between unrelated parts of your code.
The Same Problem with Set
let processedObjects = new Set();
function processObject(obj) {
if (processedObjects.has(obj)) {
console.log("Already processed");
return;
}
// Process the object...
processedObjects.add(obj);
}
let data = { id: 1, payload: new Array(1000000).fill("data") };
processObject(data);
// We're done with this data
data = null;
// But processedObjects still holds a reference!
// The large object cannot be garbage collected
console.log(processedObjects.size); // 1
This is the exact problem WeakMap and WeakSet are designed to solve.
WeakMap: Weakly Referenced Keys
A WeakMap is similar to a Map, but with three critical differences:
- Keys must be objects (or non-registered symbols). Primitives are not allowed as keys.
- Keys are held weakly. If the only reference to a key object is the WeakMap entry itself, the key (and its associated value) can be garbage collected.
- WeakMaps are not iterable and have no
sizeproperty. You cannot enumerate their contents.
Basic Usage
let weakMap = new WeakMap();
let obj1 = { name: "Alice" };
let obj2 = { name: "Bob" };
weakMap.set(obj1, "metadata for Alice");
weakMap.set(obj2, "metadata for Bob");
console.log(weakMap.get(obj1)); // "metadata for Alice"
console.log(weakMap.has(obj2)); // true
weakMap.delete(obj2);
console.log(weakMap.has(obj2)); // false
Weak Reference Behavior
The key distinction is what happens when you remove all other references to a key:
let weakMap = new WeakMap();
let user = { name: "Alice" };
weakMap.set(user, { role: "admin", loginCount: 42 });
console.log(weakMap.get(user)); // { role: 'admin', loginCount: 42 }
// Remove the only external reference to the user object
user = null;
// At some point after this, the garbage collector will:
// 1. Reclaim the { name: "Alice" } object
// 2. Automatically remove the entry from the WeakMap
// 3. Also reclaim the { role: "admin", loginCount: 42 } value
// We can't verify this directly because we can't iterate WeakMap
// But the memory IS freed, that's the whole point
Compare this with a regular Map:
let map = new Map();
let user = { name: "Alice" };
map.set(user, { role: "admin" });
user = null;
// The Map still holds the entry, memory is NOT freed
console.log(map.size); // 1 (still there!)
Only Objects as Keys
WeakMap keys must be objects. Primitive values are not allowed because primitives are not garbage collected the same way objects are:
let weakMap = new WeakMap();
let obj = { id: 1 };
weakMap.set(obj, "works"); // ✅ Object key
// ❌ TypeError: Invalid value used as weak map key
// weakMap.set("string key", "fails");
// weakMap.set(42, "fails");
// weakMap.set(true, "fails");
// weakMap.set(null, "fails");
// weakMap.set(undefined, "fails");
// Non-registered symbols work (ES2023+)
let sym = Symbol("mySymbol");
weakMap.set(sym, "works"); // ✅ Non-registered symbol
// ❌ Registered symbols (Symbol.for) do NOT work
// weakMap.set(Symbol.for("registered"), "fails");
Available Methods
WeakMap has only four methods, a deliberately minimal API:
let wm = new WeakMap();
let key = {};
wm.set(key, "value"); // Add or update entry
wm.get(key); // "value" -> retrieve value
wm.has(key); // true -> check existence
wm.delete(key); // true -> remove entry
// These do NOT exist:
// wm.size -> undefined
// wm.keys() -> not a function
// wm.values() -> not a function
// wm.entries() -> not a function
// wm.forEach() -> not a function
// wm.clear() -> not a function
// for (let x of wm) -> TypeError: wm is not iterable
WeakMap Use Cases
WeakMap's unique characteristics make it the perfect tool for several common patterns.
Use Case 1: Caching Computed Results
When you compute expensive results based on objects and want to cache them, a regular Map prevents the source objects from being garbage collected. A WeakMap lets the cache clean itself up automatically:
let cache = new WeakMap();
function computeExpensiveResult(obj) {
if (cache.has(obj)) {
console.log("Cache hit!");
return cache.get(obj);
}
console.log("Computing...");
// Simulate expensive computation
let result = {
processed: true,
hash: JSON.stringify(obj).split("").reduce((a, c) => a + c.charCodeAt(0), 0),
timestamp: Date.now()
};
cache.set(obj, result);
return result;
}
let data = { id: 1, values: [10, 20, 30] };
computeExpensiveResult(data); // "Computing..."
computeExpensiveResult(data); // "Cache hit!"
computeExpensiveResult(data); // "Cache hit!"
// When data is no longer needed:
data = null;
// The cache entry is automatically eligible for garbage collection
// No manual cleanup required!
With a regular Map, you would need to manually remove cache entries when the source data is no longer needed, or risk a memory leak. With WeakMap, the cleanup is automatic.
Use Case 2: Private Data for Objects
WeakMap provides a way to associate truly private data with objects. Since the data is keyed by object reference and WeakMap is not iterable, there is no way for external code to access or enumerate the private data:
const _private = new WeakMap();
class User {
constructor(name, email, password) {
this.name = name;
// Store sensitive data privately
_private.set(this, {
email: email,
password: password, // In real code, hash this!
loginAttempts: 0
});
}
getEmail() {
return _private.get(this).email;
}
authenticate(password) {
let data = _private.get(this);
data.loginAttempts++;
if (data.loginAttempts > 5) {
throw new Error("Account locked: too many attempts");
}
return data.password === password;
}
getLoginAttempts() {
return _private.get(this).loginAttempts;
}
}
let user = new User("Alice", "alice@example.com", "secret123");
// Public data is accessible
console.log(user.name); // "Alice"
// Private data is accessible only through methods
console.log(user.getEmail()); // "alice@example.com"
console.log(user.authenticate("wrong")); // false
console.log(user.getLoginAttempts()); // 1
// Cannot access private data directly
console.log(user.password); // undefined (not on the object)
console.log(user.email); // undefined (not on the object)
// When user is garbage collected, the private data goes with it
user = null;
// Private data in _private is automatically cleaned up
Modern JavaScript has true private fields using the # syntax (#password), which is generally preferred for new code. The WeakMap pattern for private data was the standard solution before # fields existed (pre-ES2022) and is still found in many codebases and libraries. Both approaches are valid.
Use Case 3: DOM Element Metadata
This is the corrected version of the memory-leaking example from the beginning of this article:
const elementData = new WeakMap();
function trackElement(element) {
elementData.set(element, {
createdAt: Date.now(),
clickCount: 0,
lastInteraction: null
});
}
function recordClick(element) {
let data = elementData.get(element);
if (data) {
data.clickCount++;
data.lastInteraction = Date.now();
}
}
function getClickCount(element) {
let data = elementData.get(element);
return data ? data.clickCount : 0;
}
// Usage
let button = document.createElement("button");
document.body.appendChild(button);
trackElement(button);
button.addEventListener("click", () => recordClick(button));
// Later, when the button is removed from the DOM
button.remove();
button = null;
// The WeakMap entry is automatically cleaned up!
// No memory leak, no manual cleanup code needed
Use Case 4: Memoizing Instance-Specific Computations
const computationCache = new WeakMap();
function getStats(dataset) {
if (computationCache.has(dataset)) {
return computationCache.get(dataset);
}
// Expensive computation
let values = dataset.values;
let stats = {
sum: values.reduce((a, b) => a + b, 0),
avg: values.reduce((a, b) => a + b, 0) / values.length,
min: Math.min(...values),
max: Math.max(...values),
count: values.length
};
computationCache.set(dataset, stats);
return stats;
}
let report = { id: 1, values: [10, 20, 30, 40, 50] };
console.log(getStats(report)); // Computes
console.log(getStats(report)); // Returns cached
// When report is no longer needed:
report = null;
// Cache entry is automatically eligible for GC
Output:
{ sum: 150, avg: 30, min: 10, max: 50, count: 5 }
{ sum: 150, avg: 30, min: 10, max: 50, count: 5 }
Use Case 5: Tracking Object State Without Modifying Objects
Sometimes you receive objects from external code (a library, an API) and need to track information about them without adding properties to the objects themselves:
const visited = new WeakMap();
function processNode(node) {
if (visited.has(node)) {
console.log(`Already visited node: ${node.id}`);
return visited.get(node);
}
console.log(`Processing node: ${node.id}`);
let result = { processed: true, timestamp: Date.now() };
visited.set(node, result);
return result;
}
// Graph nodes from an external library (we shouldn't modify them)
let nodeA = { id: "A", connections: [] };
let nodeB = { id: "B", connections: [] };
processNode(nodeA); // "Processing node: A"
processNode(nodeA); // "Already visited node: A"
processNode(nodeB); // "Processing node: B"
// nodeA and nodeB are not modified in any way
console.log(Object.keys(nodeA)); // ["id", "connections"] (clean!)
WeakSet: Weakly Referenced Values
A WeakSet is like a Set, but with the same weak reference behavior as WeakMap:
- Values must be objects (or non-registered symbols). No primitives.
- Values are held weakly. If the only reference to an object is the WeakSet entry, the object can be garbage collected and the entry disappears.
- WeakSets are not iterable and have no
sizeproperty.
Basic Usage
let weakSet = new WeakSet();
let obj1 = { name: "Alice" };
let obj2 = { name: "Bob" };
let obj3 = { name: "Charlie" };
weakSet.add(obj1);
weakSet.add(obj2);
console.log(weakSet.has(obj1)); // true
console.log(weakSet.has(obj3)); // false
weakSet.delete(obj2);
console.log(weakSet.has(obj2)); // false
Weak Reference Behavior
let weakSet = new WeakSet();
let user = { name: "Alice" };
weakSet.add(user);
console.log(weakSet.has(user)); // true
// Remove the only external reference
user = null;
// The object is now eligible for garbage collection
// The WeakSet entry will be automatically removed
Available Methods
WeakSet has only three methods:
let ws = new WeakSet();
let obj = {};
ws.add(obj); // Add a value
ws.has(obj); // true -> check existence
ws.delete(obj); // true -> remove value
// These do NOT exist:
// ws.size -> undefined
// ws.keys() -> not a function
// ws.values() -> not a function
// ws.entries() -> not a function
// ws.forEach() -> not a function
// ws.clear() -> not a function
// for (let x of ws) -> TypeError: ws is not iterable
Only Objects as Values
let ws = new WeakSet();
ws.add({ id: 1 }); // ✅ Object
// ❌ TypeError: Invalid value used in weak set
// ws.add("string");
// ws.add(42);
// ws.add(true);
// ws.add(null);
WeakSet Use Cases
WeakSet is more specialized than WeakMap. Its primary purpose is tagging objects to indicate they belong to a certain group or have a certain status.
Use Case 1: Tracking Visited or Processed Objects
const processed = new WeakSet();
function processOrder(order) {
if (processed.has(order)) {
console.log(`Order #${order.id} already processed - skipping`);
return;
}
console.log(`Processing order #${order.id}...`);
// ... perform processing ...
processed.add(order);
}
let order1 = { id: 101, items: ["laptop", "mouse"], total: 1099 };
let order2 = { id: 102, items: ["keyboard"], total: 79 };
processOrder(order1); // "Processing order #101..."
processOrder(order2); // "Processing order #102..."
processOrder(order1); // "Order #101 already processed - skipping"
// When orders are no longer needed:
order1 = null;
order2 = null;
// WeakSet entries are automatically cleaned up
Use Case 2: Guarding Against Circular References
When traversing object graphs (like trees or linked structures), WeakSet prevents infinite loops caused by circular references:
function deepCloneWithCircular(obj, seen = new WeakSet()) {
if (obj === null || typeof obj !== "object") {
return obj; // Primitives are returned as-is
}
// Detect circular references
if (seen.has(obj)) {
return "[Circular Reference]";
}
seen.add(obj);
if (Array.isArray(obj)) {
return obj.map(item => deepCloneWithCircular(item, seen));
}
let clone = {};
for (let key of Object.keys(obj)) {
clone[key] = deepCloneWithCircular(obj[key], seen);
}
return clone;
}
// Test with circular reference
let obj = { name: "root" };
obj.self = obj; // Circular!
obj.nested = { parent: obj }; // Another circular reference
let cloned = deepCloneWithCircular(obj);
console.log(cloned);
// {
// name: "root",
// self: "[Circular Reference]",
// nested: { parent: "[Circular Reference]" }
// }
The WeakSet seen tracks which objects have already been visited. Without it, the function would recurse infinitely. Because it is a WeakSet, the tracking data does not prevent any of the objects from being garbage collected after the cloning is complete.
Use Case 3: Type or Brand Checking
WeakSet can serve as a lightweight way to verify that an object was created by a specific constructor or factory, without modifying the object:
const validUsers = new WeakSet();
class User {
constructor(name, email) {
this.name = name;
this.email = email;
// Register this instance as a valid User
validUsers.add(this);
}
}
function sendEmail(user, message) {
// Verify the user was actually created by the User constructor
if (!validUsers.has(user)) {
throw new Error("Invalid user object - must be created with new User()");
}
console.log(`Sending "${message}" to ${user.email}`);
}
let alice = new User("Alice", "alice@example.com");
// Works with valid users
sendEmail(alice, "Welcome!");
// "Sending "Welcome!" to alice@example.com"
// Fails with fake objects
let fakeUser = { name: "Hacker", email: "hacker@evil.com" };
sendEmail(fakeUser, "Hello!");
// Error: Invalid user object - must be created with new User()
Use Case 4: Marking DOM Elements
const initialized = new WeakSet();
function initializeWidget(element) {
if (initialized.has(element)) {
console.log("Widget already initialized");
return;
}
// Set up event listeners, styles, etc.
element.classList.add("widget-active");
element.addEventListener("click", handleWidgetClick);
initialized.add(element);
console.log("Widget initialized");
}
function handleWidgetClick(e) {
console.log("Widget clicked:", e.target);
}
let widget = document.createElement("div");
initializeWidget(widget); // "Widget initialized"
initializeWidget(widget); // "Widget already initialized" - safe to call multiple times
// When the widget is removed from the DOM and all references are cleared:
widget.remove();
widget = null;
// The WeakSet entry is automatically cleaned up
// No need to manually call initialized.delete(widget)
Use Case 5: Preventing Duplicate Event Handling
const handledEvents = new WeakSet();
function handleEvent(event) {
// In complex event systems, the same event object might
// be processed by multiple handlers. Ensure we process it only once.
if (handledEvents.has(event)) {
return; // Already handled
}
handledEvents.add(event);
console.log(`Handling ${event.type} event`);
// ... process the event ...
}
Limitations: No Iteration, No Size
The weak reference behavior comes with significant trade-offs. Because the garbage collector can remove entries at any unpredictable time, WeakMap and WeakSet cannot support operations that depend on knowing their complete contents.
Why No Iteration?
Garbage collection is non-deterministic. The engine decides when to reclaim unreachable objects, and this timing varies between engine implementations, between runs of the same program, and even between different points during a single execution.
If WeakMap or WeakSet allowed iteration, the results would be unpredictable:
// Hypothetical: this does NOT work (and shouldn't)
let wm = new WeakMap();
let a = { id: 1 };
let b = { id: 2 };
let c = { id: 3 };
wm.set(a, "data-a");
wm.set(b, "data-b");
wm.set(c, "data-c");
b = null; // b is now eligible for GC
// If we could iterate:
// for (let [key, value] of wm) { ... }
// Sometimes we'd see 3 entries, sometimes 2
// depending on whether GC ran before our loop
// This non-determinism would make code unreliable
The lack of iteration is not a limitation by accident. It is a deliberate design choice that maintains program correctness.
What Is Not Available
let wm = new WeakMap();
let ws = new WeakSet();
// None of these exist:
// wm.size → undefined
// wm.keys() → TypeError
// wm.values() → TypeError
// wm.entries() → TypeError
// wm.forEach() → TypeError
// wm.clear() → TypeError (removed in the final spec)
// wm[Symbol.iterator] → undefined
// Same for WeakSet:
// ws.size → undefined
// ws.keys() → TypeError
// ws.values() → TypeError
// ws.forEach() → TypeError
// ws[Symbol.iterator] → undefined
Working Within the Limitations
Since you cannot iterate or check the size, you need to structure your code so that you always have the key (or value) available when you need to access the WeakMap or WeakSet:
// ✅ You always need the key reference to access WeakMap data
let metadata = new WeakMap();
function attachMetadata(obj, data) {
metadata.set(obj, data);
}
function getMetadata(obj) {
return metadata.get(obj); // Must have the obj reference
}
// ✅ You always need the value reference to check WeakSet membership
let validated = new WeakSet();
function validate(obj) {
validated.add(obj);
}
function isValidated(obj) {
return validated.has(obj); // Must have the obj reference
}
When Limitations Are a Problem
If you need any of the following, use a regular Map or Set instead:
- Knowing how many entries exist (size/count)
- Listing all keys or values (enumeration)
- Converting to an array (serialization)
- Clearing all entries at once (bulk operations)
- Using primitive keys (strings, numbers)
// Need to iterate? Use Map/Set and manage cleanup manually
let activeConnections = new Map(); // Not WeakMap
function addConnection(user, socket) {
activeConnections.set(user, socket);
}
function removeConnection(user) {
activeConnections.delete(user);
}
function broadcastMessage(message) {
// Iteration required - WeakMap can't do this
for (let [user, socket] of activeConnections) {
socket.send(message);
}
}
function getActiveCount() {
return activeConnections.size; // Size required - WeakMap can't do this
}
Complete Comparison: Map/Set vs. WeakMap/WeakSet
| Feature | Map | WeakMap | Set | WeakSet |
|---|---|---|---|---|
| Key/Value types | Any | Objects only (keys) | Any | Objects only |
| Key references | Strong | Weak | N/A | N/A |
| Value references | Strong | Strong (value), weak (key) | Strong | Weak |
| Garbage collection | Prevented | Allowed (for keys) | Prevented | Allowed |
size property | Yes | No | Yes | No |
| Iterable | Yes | No | Yes | No |
keys() / values() | Yes | No | Yes | No |
forEach() | Yes | No | Yes | No |
clear() | Yes | No | Yes | No |
get(key) | Yes | Yes | N/A | N/A |
set(key, val) | Yes | Yes | N/A | N/A |
add(val) | N/A | N/A | Yes | Yes |
has() | Yes | Yes | Yes | Yes |
delete() | Yes | Yes | Yes | Yes |
| Use case | General key-value storage | Caching, private data, metadata | Unique collections | Tagging, visited tracking |
Decision Guide
Ask yourself these questions:
Do you need to iterate over entries, know the size, or serialize the data?
Yes: Use Map or Set.
Are the keys/values objects that might be garbage collected?
Yes: Use WeakMap or WeakSet to avoid memory leaks.
Do you need key-value pairs or just membership tracking?
Key-value: Use WeakMap. Membership only: Use WeakSet.
Do you need primitive keys (strings, numbers)?
Yes: Use Map or Set. WeakMap/WeakSet require object keys/values.
Summary
- Regular
MapandSethold strong references to their keys and values. Objects used as keys or values cannot be garbage collected as long as the collection exists, even if all other references to them have been removed. This can cause memory leaks in long-running applications. - WeakMap holds weak references to its keys (which must be objects). When the only remaining reference to a key is the WeakMap entry itself, both the key and its associated value become eligible for garbage collection. The entry disappears automatically.
- WeakSet holds weak references to its values (which must be objects). When the only remaining reference to a value is the WeakSet entry, it becomes eligible for garbage collection and disappears.
- WeakMap use cases: caching computed results, associating private data with objects, attaching metadata to DOM elements, memoization, and tracking object state without modifying objects.
- WeakSet use cases: marking objects as visited or processed, preventing circular reference loops during traversal, brand/type checking, guarding against duplicate initialization, and tagging objects.
- Limitations: WeakMap and WeakSet are not iterable and have no
sizeproperty. You cannot enumerate their contents, clear them, or convert them to arrays. This is a deliberate design choice because garbage collection is non-deterministic. - Use
WeakMap/WeakSetwhen you need to associate data with objects that have independent lifecycles and should be allowed to be garbage collected. Use regularMap/Setwhen you need iteration, size tracking, or primitive keys.