WeakSet & WeakMap Get Certified
WeakMap and WeakSet are specialized
collections that hold weak references to
objects. Unlike regular Map and Set,
they do not prevent their contents from being garbage-collected
when nothing else points to those objects.
In this lesson you will learn the differences from their strong counterparts, the restrictions they impose, and the practical patterns they enable — private data, metadata, and tracking without memory leaks.
What Is a WeakMap?
A WeakMap stores key-value pairs where the keys
must be objects. The references to those keys are weak, so the
entry can be cleaned up automatically when the key object is no
longer reachable elsewhere.
What this code does: Create a WeakMap and store a value against an object key. The key must be an object; primitives are not allowed.
const meta = new WeakMap();
const user = { name: "Alex" };
meta.set(user, { role: "admin" });
console.log(meta.get(user));
console.log(meta.has(user));
const meta = new WeakMap(); creates an empty WeakMap. Like a regular Map, it stores key-value pairs, but its keys must be objects, and it holds them with weak references. That means the WeakMap does not keep a key object alive: if nothing else in the program refers to the object, JavaScript may remove it (and its entry) from memory.const user = { name: "Alex" }; creates an object. This object will be the key.meta.set(user, { role: "admin" }); stores an entry: the key is the user object and the value is another object, { role: "admin" }. This is a way to attach extra information to user without changing user itself.console.log(meta.get(user)); looks up the value for the key user and prints it: the object with role set to "admin".console.log(meta.has(user)); returns true if the WeakMap has an entry for this key. The output is true.WeakMap Only Accepts Object Keys
Trying to use a string, number, or other primitive as a key
throws a TypeError.
What this code does: WeakMap rejects non-object keys. The try/catch shows the error message when a string is used as a key.
const meta = new WeakMap();
try {
meta.set("userId", { role: "admin" });
} catch (err) {
console.log("Error:", err.message);
}
const meta = new WeakMap(); creates an empty WeakMap.try { starts a try block, so that we can catch the error that the next line causes, and show its message.meta.set("userId", { role: "admin" }); tries to use the string "userId" as a key. A WeakMap only accepts objects as keys, because only objects can be garbage-collected. This line throws a TypeError.} catch (err) { runs because an error was thrown. The error object is stored in err.console.log("Error:", err.message); prints the label and the error’s message. In Chrome the message reads Invalid value used as weak map key (other browsers word it a little differently).} ends the catch block. To fix the problem, use an object as the key instead of a string.Basic WeakMap Methods
The available methods are set(), get(),
has(), and delete(). There is no
size, no iteration, and no clear().
What this code does: Demonstrate set, get, has, and delete. After deletion the key is no longer present.
const cache = new WeakMap();
const element = { id: "btn-1" };
cache.set(element, { clicks: 0 });
console.log(cache.get(element));
console.log(cache.has(element));
cache.delete(element);
console.log(cache.has(element));
const cache = new WeakMap(); creates an empty WeakMap that will hold extra data for objects.const element = { id: "btn-1" }; creates an object that stands for a button. It will be the key.cache.set(element, { clicks: 0 }); stores the object { clicks: 0 } as the value for the key element.console.log(cache.get(element)); returns the value stored for element and prints it: the object with clicks set to 0.console.log(cache.has(element)); prints true, because the key exists.cache.delete(element); removes the entry for element. It returns true if something was removed, but here the return value is not used.console.log(cache.has(element)); prints false, because the entry was deleted. A WeakMap has only these four methods (set, get, has, delete).No Size and No Iteration
Because entries can disappear at any time due to garbage
collection, WeakMap deliberately does not expose
size or allow looping.
What this code does: Attempting to read size or iterate is not supported. The example simply confirms that size is undefined.
const wm = new WeakMap();
const obj = {};
wm.set(obj, 1);
console.log(typeof wm.size); // undefined
console.log("size" in wm); // false
const wm = new WeakMap(); creates an empty WeakMap.const obj = {}; creates an empty object to use as a key. Any object works.wm.set(obj, 1); stores the number 1 as the value for the key obj. The WeakMap now holds one entry.console.log(typeof wm.size); asks for wm.size. A regular Map has a size property, but a WeakMap does not, so the value is undefined and typeof reports "undefined". The output is undefined. The comment at the end of the line, // undefined, tells you the expected result. The reason is that entries can disappear at any moment when objects are garbage-collected, so a size would not be reliable.console.log("size" in wm); uses the in operator to check whether the WeakMap has a property named size at all. The output is false, matching the comment // false. For the same reason, a WeakMap cannot be looped over.Use Case: Private Instance Data
A classic pattern is to keep a WeakMap defined outside a
class to store data that should be private to each instance,
keyed by the instance itself.
What this code does: A classic use case: store private instance data in a WeakMap. The balance is hidden from the outside and cleaned up when the account object is garbage collected.
const privateData = new WeakMap();
class BankAccount {
constructor(owner, balance) {
this.owner = owner;
privateData.set(this, { balance: balance });
}
getBalance() {
return privateData.get(this).balance;
}
deposit(amount) {
const data = privateData.get(this);
data.balance += amount;
}
}
const account = new BankAccount("Michael", 100);
account.deposit(50);
console.log(account.getBalance());
console.log(account.balance);
const privateData = new WeakMap(); creates a WeakMap outside the class. It will hold private data for each account object, keyed by the object itself. Because only the class code can reach this variable, the data is hidden from the outside. Line 2 is a blank separator line.class BankAccount { declares a class, a template for creating account objects.constructor(owner, balance) { is the special method that runs when a new account is created with new. It receives the owner’s name and the starting balance.this.owner = owner; stores the owner’s name as a normal property of the new object. This one is visible from outside.privateData.set(this, { balance: balance }); uses the new object (this) as a key in the WeakMap, and stores the balance as its value. The balance is not saved on the object itself.} closes the constructor.getBalance() { defines a method that lets outside code read the balance.return privateData.get(this).balance; looks up the private data for this account and returns its balance property.} closes the method.deposit(amount) { defines a method that adds money to the account.const data = privateData.get(this); fetches the stored object for this account. It is the same object that is in the WeakMap, not a copy.data.balance += amount; adds the amount to the balance. Because data is the stored object, the WeakMap now holds the new balance.} closes the method.} ends the class. Line 16 is a blank separator line.const account = new BankAccount("Michael", 100); creates an account. The constructor stores the owner on the object and the balance of 100 in the WeakMap.account.deposit(50); adds 50 to the private balance, making it 150.console.log(account.getBalance()); uses the public method to read the private value. The output is 150.console.log(account.balance); tries to read a balance property on the object. There is none, because the balance lives only in the WeakMap. The output is undefined. When the account object is no longer used anywhere, its private entry is cleaned up automatically.
Notice that account.balance is undefined
— the actual balance lives only inside privateData,
accessible only through the class’s own methods.
Use Case: Attaching Metadata to Objects
A WeakMap is also useful for associating extra
information with an object without modifying the object itself.
What this code does: Track per-object metadata (view counts) without preventing the product object from being garbage collected when it is no longer used.
const viewCounts = new WeakMap();
function trackView(item) {
const currentCount = viewCounts.get(item) || 0;
viewCounts.set(item, currentCount + 1);
return viewCounts.get(item);
}
const productCard = { id: "P100" };
console.log(trackView(productCard));
console.log(trackView(productCard));
console.log(trackView(productCard));
const viewCounts = new WeakMap(); creates a WeakMap that will keep a view count for each object, without changing the objects themselves. Line 2 is a blank separator line.function trackView(item) { declares a function that records one view for the object passed in.const currentCount = viewCounts.get(item) || 0; looks up the count for this object. If the object has no entry yet, get() returns undefined, and the || 0 (OR) supplies 0 as the default.viewCounts.set(item, currentCount + 1); saves the count plus one as the new value for this object.return viewCounts.get(item); reads the stored count again and returns it.} ends the function body. Line 8 is a blank separator line.const productCard = { id: "P100" }; creates the object whose views we want to count.trackView(productCard) finds no entry (count 0), stores 1, and returns it. The output is 1.2.3. The product object was never modified: the counts are stored separately in the WeakMap, and disappear when the object is garbage-collected.What Is a WeakSet?
A WeakSet stores a collection of unique
objects (not primitive values), similarly held
weakly. It supports add(), has(), and
delete().
What this code does: A WeakSet holds unique objects with weak references.
add and has mark and check whether a page object has been visited.
const visitedPages = new WeakSet();
const page1 = { url: "/home" };
const page2 = { url: "/about" };
visitedPages.add(page1);
console.log(visitedPages.has(page1));
console.log(visitedPages.has(page2));
const visitedPages = new WeakSet(); creates an empty WeakSet: a collection of unique objects, held with weak references. It stores only values (no key-value pairs), and it can only hold objects.const page1 = { url: "/home" }; creates an object that represents a page.const page2 = { url: "/about" }; creates another page object. It will never be added to the set.visitedPages.add(page1); adds page1 to the set, marking it as visited.console.log(visitedPages.has(page1)); checks whether the set contains page1. The output is true.console.log(visitedPages.has(page2)); checks page2, which was never added. The output is false.WeakSet Also Requires Objects
What this code does: WeakSet only accepts objects. Adding a string throws a TypeError.
const visitedPages = new WeakSet();
try {
visitedPages.add("not an object");
} catch (err) {
console.log("Error:", err.message);
}
const visitedPages = new WeakSet(); creates an empty WeakSet.try { starts a try block so that the error can be caught.visitedPages.add("not an object"); tries to add a string. A WeakSet accepts only objects, so this throws a TypeError.} catch (err) { runs because of the error.console.log("Error:", err.message); prints the error’s message. In Chrome it reads Invalid value used in weak set (other browsers use different wording).} ends the catch block. Use an object instead of a string to avoid the error.Deleting From a WeakSet
What this code does: delete removes an object from the WeakSet.
After deletion, has returns false for that user.
const flaggedUsers = new WeakSet();
const user = { name: "Guest" };
flaggedUsers.add(user);
console.log(flaggedUsers.has(user));
flaggedUsers.delete(user);
console.log(flaggedUsers.has(user));
const flaggedUsers = new WeakSet(); creates an empty WeakSet that will hold flagged user objects.const user = { name: "Guest" }; creates the object to flag.flaggedUsers.add(user); adds the user to the set.console.log(flaggedUsers.has(user)); prints true, because the user is in the set.flaggedUsers.delete(user); removes the user from the set.console.log(flaggedUsers.has(user)); prints false, because the user is no longer in the set.Use Case: Marking Objects as Processed
A WeakSet is a natural fit for tracking which objects
have already been handled, without needing to modify those
objects with a flag property.
What this code does: Use a WeakSet to mark objects as already processed. Calling the function twice with the same order object detects the duplicate and skips reprocessing.
const processedOrders = new WeakSet();
function processOrder(order) {
if (processedOrders.has(order)) {
return "Order " + order.id + " was already processed.";
}
processedOrders.add(order);
return "Processing order " + order.id + " now.";
}
const order1 = { id: "ORD-1" };
console.log(processOrder(order1));
console.log(processOrder(order1));
const processedOrders = new WeakSet(); creates a WeakSet that remembers which order objects have already been handled. Using a set means the order objects need no extra flag property. Line 2 is a blank separator line.function processOrder(order) { declares a function that takes an order object.if (processedOrders.has(order)) { checks whether this exact order object is in the set.return "Order " + order.id + " was already processed."; returns a message and exits, so the order is not processed twice.} closes the if block.processedOrders.add(order); is reached only for new orders. It adds the order to the set so that the next call recognises it.return "Processing order " + order.id + " now."; returns the message for a newly processed order.} ends the function body. Line 10 is a blank separator line.const order1 = { id: "ORD-1" }; creates an order object.processOrder(order1) finds that the order is not in the set, adds it, and returns Processing order ORD-1 now.Order ORD-1 was already processed. The duplicate is detected.Map/Set vs. WeakMap/WeakSet
| Feature | Map / Set | WeakMap / WeakSet |
|---|---|---|
| Allowed keys/values | Any type | Objects only |
| Prevents garbage collection? | Yes (strong references) | No (weak references) |
| Has a size property? | Yes | No |
| Iterable (for…of, forEach)? | Yes | No |
| Typical use case | General-purpose collections | Private data, metadata, object tracking |
Practical Example
Let’s combine a WeakMap and a WeakSet to
track login attempts for session objects and flag suspicious
sessions after too many failures.
What this code does: Combine WeakMap (metadata) and WeakSet (flagging). Failed attempts are counted; when a threshold is reached the session is added to the flagged set.
const sessionMetadata = new WeakMap();
const flaggedSessions = new WeakSet();
function createSession(userId) {
const session = { userId: userId };
sessionMetadata.set(session, { attempts: 0 });
return session;
}
function recordFailedAttempt(session) {
const meta = sessionMetadata.get(session);
meta.attempts++;
if (meta.attempts >= 3) {
flaggedSessions.add(session);
}
}
const session = createSession("user_42");
recordFailedAttempt(session);
recordFailedAttempt(session);
recordFailedAttempt(session);
console.log(flaggedSessions.has(session));
console.log(sessionMetadata.get(session));
const sessionMetadata = new WeakMap(); will store a data object (the failed-attempt counter) for each session object.const flaggedSessions = new WeakSet(); will hold the sessions that have been flagged as suspicious. Line 3 is a blank separator line.function createSession(userId) { declares a function that creates a new session object.const session = { userId: userId }; creates the session as an object holding the user id.sessionMetadata.set(session, { attempts: 0 }); stores a counter, starting at 0, for this session in the WeakMap. The session object itself is left unchanged.return session; gives the session object back to the caller.} ends the function. Line 9 is a blank separator line.function recordFailedAttempt(session) { declares a function that is called after each failed login.const meta = sessionMetadata.get(session); fetches the counter object stored for this session.meta.attempts++; adds 1 to the attempts. This changes the stored object, so the WeakMap holds the updated count.if (meta.attempts >= 3) { checks whether there have been three or more failures.flaggedSessions.add(session); adds the session to the WeakSet, marking it as suspicious.} closes the if block.} ends the function. Line 17 is a blank separator line.const session = createSession("user_42"); creates a session for the user, with an attempts counter of 0.recordFailedAttempt(session); raises the attempts to 1. It is below 3, so no flag is set.flaggedSessions.console.log(flaggedSessions.has(session)); prints true.console.log(sessionMetadata.get(session)); prints the stored object, which shows attempts as 3. If session were no longer referenced anywhere, both its metadata and its flag would be eligible for automatic cleanup.
If session were ever discarded and no longer
referenced anywhere else in the program, both its metadata entry
and its flagged status would automatically become eligible for
cleanup — no manual removal required.
Common Mistakes With WeakMap and WeakSet
- Trying to use a string, number, or other primitive as a
WeakMapkey orWeakSetvalue, which throws aTypeError. - Expecting to loop over a
WeakMaporWeakSet, or check itssize, when neither feature is supported. - Using
WeakMap/WeakSetwhen a regularMap/Setwas actually needed, simply because “weak” sounds like a performance optimization rather than a specific memory-management tool. - Assuming entries disappear immediately once an object is no longer used elsewhere — garbage collection timing is not predictable or immediate.
- Storing primitive identifiers (like a numeric ID) instead of the actual object reference, which defeats the purpose of using a weak collection in the first place.
Best Practices for WeakMap and WeakSet
- Use
WeakMapfor storing private or auxiliary data tied to the lifetime of specific objects. - Use
WeakSetfor tracking membership or “has this been seen/processed” status for a group of objects. - Reach for a regular
MaporSetwhen you need to iterate over entries, check the size, or use non-object keys. - Don’t rely on
WeakMap/WeakSetfor logic that must run at a predictable time — garbage collection isn’t something your code should depend on directly. - Keep the object being used as a key/value as the actual reference you care about, not a copy or a derived identifier.
Quick Summary
WeakMapstores key-value pairs where keys must be objects, held with weak references.WeakSetstores a collection of unique objects, also held weakly.- Both support
set()/add(),get()(WeakMap only),has(), anddelete(). - Neither supports iteration or a
sizeproperty, since entries can disappear as objects get garbage collected. - Common use cases include private instance data, metadata tracking, and marking objects as already processed.
- Use regular
MaporSetinstead when you need iteration, size checks, or non-object keys.
Try It Yourself
Modify the code below. Try creating a second product and tracking
its view count separately, or resetting a product’s count with
delete().
What this code does: Practice example: track views for a product, then delete the entry. Try a second product or reset a count, then click Test My Code.
const viewCounts = new WeakMap();
function trackView(item) {
const currentCount = viewCounts.get(item) || 0;
viewCounts.set(item, currentCount + 1);
return viewCounts.get(item);
}
const productA = { id: "P200" };
console.log(trackView(productA));
console.log(trackView(productA));
viewCounts.delete(productA);
console.log(viewCounts.has(productA));
const viewCounts = new WeakMap(); creates a WeakMap that holds a view count for each object. Line 2 is a blank separator line.function trackView(item) { declares a function that records one view for an object.const currentCount = viewCounts.get(item) || 0; reads the stored count, or uses 0 if there is none yet.viewCounts.set(item, currentCount + 1); saves the count plus one.return viewCounts.get(item); returns the updated count.} ends the function. Line 8 is a blank separator line.const productA = { id: "P200" }; creates the product object. Try creating a second product and counting its views separately.trackView(productA) stores 1 and prints 1.trackView(productA) stores 2 and prints 2.viewCounts.delete(productA); removes the entry for this product, which resets its count.console.log(viewCounts.has(productA)); prints false, because the entry was deleted. The next call to trackView(productA) would start again from 1.Tip: A simple way to decide: if you ever need to list, count, or loop over everything stored in the collection, you need a regularMaporSet. If you only ever need to look something up by a specific object, a weak collection is a good fit.
Understanding WeakMap and WeakSet is
useful before moving on to memory management concepts, classes,
and more advanced JavaScript topics.
🧪 Test Your JavaScript Code
Edit the code on the left and click “Run Code” to see the console output on the right.
Click “Run Code” to see the output here.
Code Explanation
The complete example stays visible. Use Previous and Next to move through its logical blocks.
Select Next to begin exploring the code.