When would you reach for a JavaScript WeakSet, and what can you not do with one?
answer
- marking, not storing
- have I already seen this object
- frozen targets and name collisions
- add, has, delete — nothing else
- no payload; that is WeakMap's job
basics
~20 sUse a WeakSet to mark objects — already visited, already validated, created by your factory — without keeping them alive or touching them. You get only add, has and delete: no size, no iteration, and members must be objects.
solid answer
~50 sA `WeakSet` answers exactly one question — "have I seen this object?" — for objects whose lifetime you do not own. The classic uses are cycle guards during recursive traversal, one-shot marking such as "this node has already been initialised", and brand checks where a factory records the instances it produced so a later function can verify provenance without trusting `instanceof`. It works where a marker property would not: the target may be frozen, a property name could collide, and an added flag would show up in `Object.keys`, spread and `JSON.stringify`. The limits mirror `WeakMap`: members must be objects (or unregistered symbols since ES2023), the API is only `add`, `has` and `delete`, there is no `size`, no iteration and no `clear()`, and — since it stores no values — it can record membership but not data. If you need to count the marked objects or list them, you need a `Set` and you have taken responsibility for removing entries.
code
javascript · 17 linesconst brand = new WeakSet();
function createToken(payload) {
const t = Object.freeze({ payload });
brand.add(t);
return t;
}
function isToken(v) {
return typeof v === 'object' && v !== null && brand.has(v);
}
const real = createToken('abc');
const fake = Object.freeze({ payload: 'abc' });
console.log(isToken(real)); // true
console.log(isToken(fake)); // false — never added by the factorygo deeper
Remember the three methods — add, has, delete — that members must be objects, and that a WeakSet records membership only, with no value attached.
Explain why weak marking beats a flag property: frozen targets, name collisions, and visibility in Object.keys, spread and JSON — plus the fact that the record cannot extend the object's life.
Show judgment about where marking belongs in a long-running system, and recognise when the requirement to count or enumerate means a Set with an explicit removal point rather than a weak structure.
Treat brand checks and provenance as an API-boundary decision — what your module is willing to trust from callers — and weigh a module-private WeakSet against prototype checks or nominal typing for the guarantee it actually gives.
## The one job it does well `WeakSet` is membership without ownership. It records that an object belongs to some group, holds it weakly so the record cannot extend its life, and offers three methods: ```js const ws = new WeakSet(); const obj = {}; ws.add(obj); // returns the set, so calls chain ws.has(obj); // true ws.delete(obj); // true ws.size; // undefined — no count exists ``` Members must be objects, or unregistered symbols from ES2023 onward; `ws.add('x')` throws a `TypeError`. ## Cycle guards The most common real use is stopping infinite recursion over a possibly cyclic object graph: ```js function walk(value, seen = new WeakSet()) { if (value === null || typeof value !== 'object') return; if (seen.has(value)) return; // already visited on this path seen.add(value); for (const child of Object.values(value)) walk(child, seen); } ``` A `Set` would work here too, because the guard is short-lived, but a `WeakSet` costs nothing extra and cannot outlive the traversal even if someone hoists it to module scope later. The habit is cheap insurance. ## Marking objects you do not own When the group is "objects that have already been processed", the marker must not mutate the object and must not keep it alive: ```js const initialised = new WeakSet(); export function setUp(target) { if (initialised.has(target)) return; // idempotent initialised.add(target); // ... one-time work } ``` The alternative — `target.__initialised = true` — fails on a frozen object, collides with names the owner may add, and leaks into `Object.keys`, spread and serialisation. A `Set` avoids the collision but pins every target for the module's lifetime; `WeakSet` does not. ## Brand checks A factory can record what it produced, so a later function can verify provenance: ```js const brand = new WeakSet(); export function createToken(payload) { const t = Object.freeze({ payload }); brand.add(t); return t; } export function isToken(v) { return typeof v === 'object' && v !== null && brand.has(v); } ``` This is stronger than `instanceof`, which can be fooled by an object whose prototype was set deliberately, and stronger than a marker property, which anyone can copy onto a forgery. Because the set is module-private and not enumerable, only this module can add to it, and a hand-built object can never be a member. ## What you give up The restrictions follow from the weakness, exactly as with `WeakMap`. Entries can be reclaimed at a moment the specification does not define, so exposing a count or an iterator would make garbage-collection timing observable, and the language provides neither: no `size`, no `forEach`, no `values()`, no `Symbol.iterator` — so `[...weakSet]` throws — and no `clear()`; to drop everything, allocate a new set. There is also no data. A `WeakSet` is a membership test only. The moment you want to attach *something* to each object — a count, a timestamp, cached output — you want a `WeakMap`, which does the same weak keying and gives you a value slot. Many codebases skip `WeakSet` entirely for this reason and use `WeakMap` with a trivial value, which is a defensible style choice; `WeakSet` simply states the intent more precisely when there genuinely is no payload. And, as always, members must be objects. "I have already seen this user id" keyed by a string needs a `Set` or `Map` with an eviction rule you write yourself, because primitives have no lifetime for the collector to track. ## Choosing between Set and WeakSet Ask two questions. *Will I ever need to enumerate or count this group?* If yes, you need a `Set` regardless of the leak risk, and must manage removal explicitly. *Do the members outlive the group, or does the group outlive the members?* If the collection is long-lived and its members are transient — nodes, requests, instances — weakness is the difference between a flat heap and a climbing one.
- When should you use a WeakMap with a dummy value instead of a WeakSet?Whenever there is any chance you will want to attach data later — a timestamp, a count, cached output. WeakSet stores membership only, so growing a payload means a refactor. If the group is genuinely payload-free, WeakSet documents that intent more precisely, and both have identical lifetime behaviour.
- Why is a WeakSet brand check harder to forge than instanceof?`instanceof` only walks the prototype chain, and anyone can build an object with `Object.create(Token.prototype)` or reassign a prototype to pass it. Membership in a module-private WeakSet can only be granted by code inside that module calling `add`, and the set is neither exported nor enumerable, so a hand-built object can never be a member.
- Can you use a WeakSet to deduplicate a list of user id strings?No — WeakSet rejects primitives with a TypeError, since a string has no collectable identity. Use a plain `Set` for that, and if it is long-lived, bound it yourself with a size cap, a TTL, or deletion at a known lifecycle point.
saying these in an interview costs you the question
- Says WeakSet can be iterated to list marked objects
- Tries to add strings or numbers as members
- Claims WeakSet stores a value per member
- Uses a marker property on a frozen object instead
- Believes instanceof is as forgery-resistant as a brand set