WeakMap & WeakSet
Attach data to objects without keeping them alive: use WeakMap for per-object caches and private data, WeakSet for tracking objects, and non-registered symbols as modern weak keys.
- 01Explain weak reachabilitySay why a WeakMap entry does not keep its object key alive.
- 02Use WeakMap and WeakSet APIsSet, get, has, and delete with valid keys and recognize TypeErrors.
- 03Choose the right collectionPick Map, WeakMap, Set, or WeakSet for caches, metadata, and tracking.
Objects can carry quiet notes
A Map remembers a value for any key. That is perfect when you want a collection you can count, loop over, and keep. A WeakMap is different: it attaches data to an object without making that object stay alive just because the collection knows about it.
The curriculum summary says it well: attach data to objects without keeping them alive. That one sentence explains the three big uses in this lesson: per-object caches, private data for instances, and tracking which objects have been processed.
A WeakMap stores values by object or non-registered symbol key, but the key is held weakly: if nothing else can reach the key, the entry can disappear. A WeakSet stores weak membership for objects.
Imagine a guest wearing a name tag. A regular Map keeps the guest listed. A WeakMap can forget the tag after the rest of the program has forgotten the guest.
- In real life: A guest with a name tag
- In JavaScript: An object used as a key
- In real life: A guest list keeps each guest listed
- In JavaScript: A
Mapkeeps key objects strongly reachable - In real life: The tag goes when the guest leaves
- In JavaScript: A
WeakMapdoes not keep the key alive - In real life: No public list of old tags
- In JavaScript: No
size,keys, or iteration
Where the analogy stops: Event staff decide when a guest leaves. JavaScript decides when cleanup happens, and normal code cannot see the exact moment.
This lesson builds on Map and Set, the Symbols lesson, Objects & references, and Closures. Deeper lessons named Memory management & garbage collection and Weak references & ephemerons come later; here, “reachable” simply means “your program can still get to the object through variables or other objects.”
Weakly held keys
STEP THROUGHWeakMap does not mean “small Map” or “slow Map.” It means the collection’s link to a key is weak. When the key object is still reachable elsewhere, get, has, set, and delete work normally. The difference appears when the rest of your program stops referencing the key.
Step through the same object used as a Map key and a WeakMap key. The difference is reachability, not the get result while the object is still alive.
script
const strong = new Map();const weak = new WeakMap(); strong.set(profile, "kept by Map");weak.set(profile, "attached weakly"); console.log(strong.get(profile));console.log(weak.get(profile));Notice what the replay does not do: it does not show an entry disappearing at a specific moment. Garbage collection timing is not observable in normal JavaScript. The lesson can show the rules and a reachability model, but not the engine’s private memory schedule.
The real API rules
REAL CODEWeakMap has a deliberately tiny API: set, get, has, and delete. WeakSet mirrors Set’s membership operations: add, has, and delete. The key rule is strict: WeakMap keys must be objects or non-registered symbols; WeakSet values must be objects.
Run the API rules from real code. Notice the successful object and non-registered symbol keys, then the two TypeErrors.
script
const weakSet = new WeakSet();const obj = {};const localSymbol = Symbol("token");const registered = Symbol.for("token"); weakMap.set(obj, "object ok");weakSet.add(obj);weakMap.set(localSymbol, "symbol ok"); console.log(weakMap.get(obj));console.log(weakSet.has(obj));console.log(weakMap.get(localSymbol));console.log(typeof weakMap.size);console.log(typeof weakMap.keys); try { weakMap.set("id", 1); } catch (error) { console.log(error.name); }try { weakMap.set(registered, 1); } catch (error) { console.log(error.name); }| Collection | Works with | Has | Does not have |
|---|---|---|---|
WeakMap | Object keys and non-registered symbol keys | set, get, has, delete | size, keys, values, entries, forEach |
WeakSet | Object values | add, has, delete | size, iteration, forEach |
If JavaScript exposed the list or count of WeakMap keys, code could learn when the engine collected unreachable objects. The language keeps that timing private, so WeakMap and WeakSet are intentionally non-iterable.
A conceptual reachability model
INTERACTIVEThink in arrows. Variables, object properties, arrays, closures, and Maps can all point to objects. If some chain of arrows from running code can reach an object, it is reachable. When no strong arrows remain, it becomes eligible for garbage collection. A WeakMap key arrow is not a strong arrow.
let user = { name: "Ada" };const notes = new WeakMap();notes.set(user, "cached stats"); user = null; // drop the last variable referenceuser→ object{ name: "Ada" }reachableThe variable user points at the object, so both Map and WeakMap lookups would work. This is a conceptual reachability model, not memory inspection.
The model uses cards, not engine memory. “Eligible” means the engine is allowed to clean up later. It does not mean cleanup has already happened, and it does not give you a callback.
Per-object caches
STEP THROUGHA cache remembers an expensive result so the next call can reuse it. If the input is an object, a WeakMap makes a good per-object cache: the result belongs to that exact object, but the cache should not keep old objects alive forever.
Step through a per-object cache. The same user object misses once, then hits without keeping the user alive forever.
script
let calculations = 0; function userStats(user) { if (statsCache.has(user)) return statsCache.get(user); calculations = calculations + 1; const stats = { label: user.name, score: user.visits * 10 }; statsCache.set(user, stats); return stats;} const ada = { name: "Ada", visits: 3 };console.log(userStats(ada).score);console.log(userStats(ada).score);console.log(calculations);This pattern appears when you compute derived data for user records, parse metadata for syntax tree nodes, or measure elements in a page. If a later part of your app still holds the object, the cache works. If the object is gone, the cache is allowed to fade with it.
- Need to list every key later
- Cache measurements for page nodes you do not own
- Count how often each word string appears
- Attach validation notes to user objects from another library
- Show the number of players currently in the table
- Store internal state for class instances in older code
Choose Map when you need iteration, counting, or primitive keys. Choose WeakMap for side data attached to object lifetimes.
Private data
COMPAREBefore JavaScript had #private fields, libraries often put a WeakMap outside a class and used each instance as the key. The public object stayed clean, and code outside the module could not read the WeakMap unless it had access to the variable holding it.
const privateData = new WeakMap(); class Counter { constructor(name) { privateData.set(this, { name, count: 0 }); } click() { const data = privateData.get(this); data.count = data.count + 1; return data.name + ": " + data.count; }} const counter = new Counter("save");console.log(counter.click());console.log(counter.count);| Pattern | Good at | Trade-off |
|---|---|---|
| Closure private state | A factory returns functions that remember hidden variables | Great for functions; many instances may duplicate methods |
| WeakMap private data | Class-like instances before #private fields | Needs a module-scoped WeakMap and careful this use |
#private fields | Modern class-private data enforced by the language | Works only inside class syntax and is covered in Private fields & methods |
Compare this with closures from the Closures lesson: closures hide variables because functions remember their lexical environment. WeakMap private data hides a lookup table instead. In modern classes, #private fields are usually clearer.
WeakSet: a “seen this object” stamp
INTERACTIVEWeakSet is the membership-only sibling: it answers “have I seen this object?” without storing extra values. Use it for objects you process once, such as initialized widgets, visited graph nodes, or page nodes you do not own.
A venue stamps a guest’s hand after entry. The stamp says “already processed” for that guest. When the guest leaves for good, the stamp leaves too. WeakSet has the same spirit for object membership.
- In real life: The guest
- In JavaScript: An object
- In real life: The hand stamp
- In JavaScript: WeakSet membership
- In real life: Checking the stamp at the door
- In JavaScript:
weakSet.has(object)
Where the analogy stops: A real stamp is visible and can be counted by looking at the room. WeakSet membership is intentionally not listable or countable.
const processed = new WeakSet(); function initialize(card) { if (processed.has(card)) return "already initialized"; processed.add(card); return "initialized now";} const card = { id: "signup" };initialize(card);initialize(card);Not run yet
The WeakSet can answer has(card), but it cannot tell you every card it contains.
Press Initialize. The first call will add the object to the WeakSet.
Symbols as WeakMap keys
ES2023Modern JavaScript allows non-registered symbols as WeakMap keys. A symbol created with Symbol("local") can be unreachable when nobody keeps it. A symbol from Symbol.for("name") lives in the global symbol registry, so it is not allowed as a weak key.
const local = Symbol("local");const shared = Symbol.for("shared");const metadata = new WeakMap(); metadata.set(local, "ok");console.log(metadata.get(local)); try { metadata.set(shared, "nope"); }catch (error) { console.log(error.name); }This connects directly to the Symbols lesson: Symbol() makes a unique local symbol, while Symbol.for() searches or creates a shared registered symbol. WeakMap accepts the first kind, not the second.
Common misconceptions
- “WeakMap is just a Map with fewer methods.” The missing methods are the point: they keep garbage collection timing unobservable.
- “I can force a WeakMap entry to disappear.” You can drop references; the engine decides when collection happens.
- “WeakMap keys can be any value.” Primitive strings, numbers, booleans, null, and undefined throw TypeError.
- “WeakSet is for unique primitive values.” That is Set’s job. WeakSet stores object membership only.
- “Private data should always use WeakMap.” In modern class code, #private fields are usually the better default.
Practice exercises
5 EXERCISESRun or read the snippet. What error name is printed?
const wm = new WeakMap();
try { wm.set("id", 1); }
catch (error) { console.log(error.name); }const wm = new WeakMap();
try { wm.set("id", 1); }
catch (error) { console.log(error.name); }"id" is a string primitive, so weakMap.set throws a TypeError.
Explain why the calculation counter does not become 2.
const cache = new WeakMap();
let calls = 0;
function score(user) {
if (cache.has(user)) return cache.get(user);
calls = calls + 1;
const result = user.visits * 10;
cache.set(user, result);
return result;
}
const ada = { visits: 4 };
console.log(score(ada));
console.log(score(ada));
console.log(calls);const cache = new WeakMap();
let calls = 0;
function score(user) {
if (cache.has(user)) return cache.get(user);
calls = calls + 1;
const result = user.visits * 10;
cache.set(user, result);
return result;
}
const ada = { visits: 4 };
console.log(score(ada));
console.log(score(ada));
console.log(calls);The first call computes and stores 40. The second call hits the WeakMap and returns without incrementing calls, so the last line prints 1.
What are the two printed words?
const processed = new WeakSet();
function init(item) {
if (processed.has(item)) return "skip";
processed.add(item);
return "init";
}
const card = {};
console.log(init(card));
console.log(init(card));const processed = new WeakSet();
function init(item) {
if (processed.has(item)) return "skip";
processed.add(item);
return "init";
}
const card = {};
console.log(init(card));
console.log(init(card));The first call adds the object and returns init. The second sees the same object already in the WeakSet and returns skip.
Fill in the missing phrase.
WeakMap has no observable size because the number of live keys can change when the engine collects unreachable objects. Exposing that number would reveal collection timing.
You need to count how many times each word string appears and print a report. Which collection should you choose?
Use Map. WeakMap does not accept string keys and cannot report a count or list entries.
Check your understanding
7 QUESTIONSQuestion 1 of 7What makes a WeakMap key weak?
Choose an answer to see the explanation.
Question 2 of 7What does the WeakMap lookup print?
Read the code, then predictconst wm = new WeakMap(); const key = {}; wm.set(key, "ok"); console.log(wm.get(key));Choose an answer to see the explanation.
Question 3 of 7Which WeakMap operation throws?
Read the code, then predictconst wm = new WeakMap(); try { wm.set("id", 1); } catch (error) { console.log(error.name); }Choose an answer to see the explanation.
Question 4 of 7Why does WeakMap have no
sizeorkeys()?Choose an answer to see the explanation.
Question 5 of 7What does the WeakSet snippet print?
Read the code, then predictconst seen = new WeakSet(); const card = {}; seen.add(card); console.log(seen.has(card)); console.log(typeof seen.size);Choose an answer to see the explanation.
Question 6 of 7Which symbol can be a WeakMap key in modern JavaScript?
Choose an answer to see the explanation.
Question 7 of 7For a cache keyed by user objects that should not keep old users alive, choose:
Choose an answer to see the explanation.
Key takeaways
- WeakMap keys are weakly held: the entry does not keep an object key alive.
- WeakMap is for per-object values; WeakSet is for object membership.
- No
size,keys, or iteration: collection timing stays unobservable. - Non-registered symbols can be WeakMap keys; registered symbols from
Symbol.forcannot. - For modern class-private data, prefer
#privatefields unless you need the WeakMap pattern.
Final definition: WeakMap and WeakSet attach information to object lifetimes without turning that information into a strong reason for the objects to stay reachable.
Up next: JSON.