Map stores values by key, while Set stores unique members. Both are directly iterable and preserve insertion order.
Object keys and members compare by identity, not by fields. A get() result of undefined also doesn’t prove that a key is absent.
Define a canonical key representation, use has() to distinguish absence, and use Node 24’s native Set methods for set algebra.
What it is and why it exists
A JavaScript Map is an ordered map that associates each unique key with a value. Keys and values can be any JavaScript value; keys don’t first need to become property names. A Set holds unique values and represents membership rather than elements at particular positions.
Plain objects also associate names with values, but their own property keys can only be strings or Symbols. Other values, including numbers, are converted to property keys, and an object can’t remain an object when used as a property key. Objects fit records with known fields; Map fits keys discovered at runtime, object keys, and key-value collections that you need to iterate directly.
Arrays retain order and duplicates, but they don’t enforce uniqueness. Repeatedly calling includes() for membership treats a sequence as a set and obscures the intent. Set puts uniqueness and has() membership in the data structure’s contract while retaining insertion-order iteration.
WeakMap and WeakSet are weak collections . They let metadata or markers follow the reachable lifetime of object keys without exposing enumeration or a size. A weak collection is not a lighter general-purpose Map or Set, because you can’t list the entries that are currently alive.
These four collections solve different modeling problems. First ask whether the data is a record, sequence, mapping, or membership set, then ask whether the collection should keep its keys alive. Similar method names don’t make the structures interchangeable.
Choose the collection shape
| Requirement | Prefer | Reason |
|---|---|---|
| Business record with fixed fields | Object | Natural property access, destructuring, and JSON shape |
| Runtime keys mapped to values | Map | Arbitrary key types and direct entry iteration |
| Ordered sequence with duplicates | Array | Indexing and sequence transformations |
| Unique members and set relations | Set | Uniqueness, membership, and set operations |
| Metadata that follows an object key’s reachability | WeakMap | The collection doesn’t keep the key alive |
| Marker that follows an object’s reachability | WeakSet | Records only whether an object is marked |
This table describes semantics, not a performance ranking. Actual speed depends on the engine, key distribution, collection size, and operation mix. Choose the structure that expresses the invariant, then measure a hot path.
How it works
Four interfaces
The core Map operations are set(key, value), get(key), has(key), and delete(key). Its size reports the entry count, and clear() removes every entry. set() returns the receiver, so calls can be chained. delete() reports whether an existing key was actually removed.
The corresponding core Set operations are add(value), has(value), and delete(value). add() also returns the receiver, and adding an existing value doesn’t increase size. clear() removes every member.
| Collection | Contents | Enumerable | Size | Write operation |
|---|---|---|---|---|
Map | Keys and values | Yes | size | set() |
Set | Unique values | Yes | size | add() |
WeakMap | Weak keys and values | No | None | set() |
WeakSet | Weak values | No | None | add() |
The default Map iterator and entries() both produce [key, value] pairs. keys() and values() produce only their respective side. The default Set iterator and values() produce members; for consistency with Map, its keys() also produces members and entries() produces [value, value] pairs.
Construction and copying
new Map(iterable) builds a mapping from iterable key-value pairs, such as a two-dimensional array, another Map, or Object.entries(record). new Set(iterable) reads each value from an iterable, so arrays, strings, and another Set all work as input. Both constructors create entries in read order.
new Map(existingMap) and new Set(existingSet) create new collection containers, but they don’t deep-copy contained objects. If keys, members, or values are objects, the old and new collections still reference the same objects. A field mutation on one of those objects is observable through both collections.
Object.fromEntries(map) fits a mapping whose string or Symbol keys should become a record. Other keys are converted to property keys, so object keys can all collapse into one "[object Object]" property. Confirm that this information loss matches the interface contract before converting.
Equality rules
Map keys and Set members both use SameValueZero equality. It mostly behaves like ===, except that all NaN values are equal and +0 equals -0. A collection can therefore contain only one NaN and one zero value.
Objects, arrays, and functions compare by object identity . Two objects with equal fields are different keys or members unless they are the same object. Collections don’t invoke a custom hash function or perform automatic deep content comparison.
| Left | Right | Same in Map or Set |
|---|---|---|
NaN | NaN | Yes |
+0 | -0 | Yes |
7 | '7' | No |
| One object reference | The same object reference | Yes |
{ id: 7 } | { id: 7 } | No |
If the business rule deduplicates by id, use the stable id as the key or first produce an explicit canonical key. Storing a temporary object as a key and reconstructing an equal-looking object for lookup won’t find the entry. Using JSON.stringify() as an improvised composite key also requires rules for property order, missing values, and unsupported types.
Insertion order and iteration
Map and Set iterate in the order of first successful insertion. Updating an existing Map value doesn’t move its key, and adding an existing Set member doesn’t move it. Deleting and inserting again creates a new order position at the current end.
That order is collection semantics, not business sorting. If display output must follow time, priority, or localized names, convert entries to an array and write the comparison rule. Depending on accidental arrival order makes output change with an upstream query or network batch.
Iterators read a live collection, not a snapshot captured at creation. An entry deleted before it is visited won’t appear; one added before iteration ends may appear. Deleting an already visited entry and inserting it again can cause it to be visited at its new position.
Updating the current key’s value during traversal is usually understandable; reordering keys during traversal is much harder to review. If a loop must delete and reinsert, iterate a [...map] or [...set] snapshot, or record changes in a separate collection and apply them after the loop.
Absence and undefined
map.get(key) returns undefined when a key is absent, but a present key may also explicitly store undefined. The return value alone can’t distinguish these states. Call map.has(key) when that distinction matters, then read the value.
map.get(key) || fallback also treats valid values such as 0, false, and the empty string as absent. Use ?? only when both null and undefined mean absence; when “not stored” differs from “stored as undefined,” you still need has().
Counting code often uses counts.set(key, (counts.get(key) ?? 0) + 1). The ?? is appropriate because the counter contract doesn’t treat undefined as a stored count. If the value domain genuinely allows undefined, spell out the missing-key branch.
Native set operations
Node 24’s Set provides union(), intersection(), difference(), and symmetricDifference(). They return new sets without mutating either input. The relation methods isSubsetOf(), isSupersetOf(), and isDisjointFrom() return Booleans. Their names expose set algebra more clearly than scattered combinations of spread, filter(), and every().
| Expression | Meaning | Result type |
|---|---|---|
a.union(b) | Member of at least one side | Set |
a.intersection(b) | Member of both sides | Set |
a.difference(b) | Member of a but not b | Set |
a.symmetricDifference(b) | Member of exactly one side | Set |
a.isSubsetOf(b) | Whether every a member is in b | boolean |
a.isSupersetOf(b) | Whether every b member is in a | boolean |
a.isDisjointFrom(b) | Whether the sides share no member | boolean |
The receiver must be an actual Set; the argument only needs the set-like protocol: a numeric size, a has() method, and a keys() method that returns a member iterator. Map satisfies that protocol and behaves as a set of its keys. Arrays don’t qualify because they lack size and has(), and their keys() produces indices.
Weak-collection boundaries
In Node 24, WeakMap keys and WeakSet members must be garbage-collectable values: objects or non-registered Symbols. A registered symbol from Symbol.for() isn’t allowed because the global registry makes it retrievable again. Strings, numbers, Booleans, null, and undefined can’t be weak keys either.
A key isn’t kept reachable merely because it is in a weak collection. After the object is no longer reachable elsewhere in the program, the engine may collect it and make the associated entry disappear. The engine decides when collection happens; an application can’t observe or force that moment.
Because collection time is uncertain, weak collections have no size, clear(), or iterator. If keys were enumerable, code could observe garbage-collection decisions or make keys reachable again while inspecting them. Use an ordinary Map with explicit lifetime management when you must list a cache, enforce capacity, or produce metrics.
WeakMap fits parsed results, DOM metadata, or external state associated with an object without modifying that object. WeakSet fits the question “has this object been processed?” Neither replaces explicit unsubscription, transaction closing, or sensitive-data cleanup.
Examples
Key identity and unique members
The first example puts equal-looking order objects, a numeric key, and a string key in a Map, then observes SameValueZero deduplication in a Set. Its output depends only on language rules, not on an object’s debug display format.
const firstOrder = { id: 'A-17' };
const sameFields = { id: 'A-17' };
const statusByOrder = new Map();
statusByOrder.set(firstOrder, 'queued');
statusByOrder.set(7, 'numeric key');
statusByOrder.set('7', 'string key');
console.log(statusByOrder.get(firstOrder));
console.log(statusByOrder.get(sameFields));
console.log(statusByOrder.get(7));
console.log(statusByOrder.get('7'));
const observed = new Set([NaN, NaN, 0, -0, '0']);
console.log(observed.size);
console.log([...new Set(['draft', 'sent', 'draft'])].join(','));queued
undefined
numeric key
string key
3
draft,sentfirstOrder finds the entry because the lookup uses the same object. sameFields only has equal fields, so the result is undefined. The final set contains three distinct members: NaN, zero, and the string '0'.
Build an index and a member set together
This example uses a Map for a primary-key order index and a Set for deduplicated reviewer IDs. A repeated order is an input-contract violation, so the function throws before overwriting an older entry.
function indexOrders(orders) {
const byId = new Map();
const reviewerIds = new Set();
for (const order of orders) {
if (byId.has(order.id)) {
throw new Error(`duplicate order: ${order.id}`);
}
byId.set(order.id, order);
for (const reviewerId of order.reviewerIds) {
reviewerIds.add(reviewerId);
}
}
return { byId, reviewerIds };
}
const orders = [
{ id: 'A-17', total: 18, reviewerIds: ['u1', 'u2'] },
{ id: 'A-18', total: 24.5, reviewerIds: ['u2', 'u3'] },
];
const index = indexOrders(orders);
console.log(index.byId.get('A-18').total);
console.log([...index.reviewerIds].join(','));
console.log([...index.byId.keys()].join(' -> '));24.5
u1,u2,u3
A-17 -> A-18u2 appears on two orders, but the Set retains it once. Both collections keep first-insertion order, so the output is predictable. A business requirement to sort reviewers by name would still need a separate sorting step.
Compose eligibility sets
Set operations can express an eligibility rule directly as membership. The ready list contains people who are eligible and trained but not blocked, without mutating any of the three inputs.
const eligible = new Set(['ana', 'bo', 'chen']);
const trained = new Set(['bo', 'chen', 'dara']);
const blocked = new Set(['chen']);
const ready = eligible
.intersection(trained)
.difference(blocked);
console.log([...ready].join(','));
console.log([...eligible.union(trained)].join(','));
console.log([...eligible.symmetricDifference(trained)].join(','));
console.log(ready.isSubsetOf(eligible));
console.log(ready.isDisjointFrom(blocked));bo
ana,bo,chen,dara
ana,dara
true
trueready contains only bo; it is a subset of eligible and disjoint from blocked. union() and symmetricDifference() return new sets, so the original rule sets remain available for later work.
Tie metadata to object lifetime
This example records per-schema validation counts in a WeakMap and prevents repeat visits to one order object with a WeakSet. A local, non-registered Symbol is also a valid weak key in Node 24.
const validationRuns = new WeakMap();
const visited = new WeakSet();
function validate(schema, input) {
const previous = validationRuns.get(schema) ?? 0;
validationRuns.set(schema, previous + 1);
const valid = schema.required.every((key) => Object.hasOwn(input, key));
return `${schema.name}:${valid}:${validationRuns.get(schema)}`;
}
function visitOnce(record) {
if (visited.has(record)) return false;
visited.add(record);
return true;
}
const orderSchema = { name: 'order', required: ['id', 'total'] };
const order = { id: 'A-17', total: 18 };
console.log(validate(orderSchema, order));
console.log(validate(orderSchema, { id: 'A-18' }));
console.log(visitOnce(order), visitOnce(order));
const requestMarker = Symbol('request');
validationRuns.set(requestMarker, 1);
console.log(validationRuns.has(requestMarker));order:true:1
order:false:2
true false
trueThe count belongs to the identity of orderSchema, not to the content of its fields. The example doesn’t try to prove that garbage collection occurred; ordinary program output can’t reliably verify that timing.
Pitfalls
Looking up with a new equal-looking object
Fix: if id defines business identity, use id itself as the key. If object keys are required, retain and pass the canonical object reference, and document the identity semantics in the interface.
Treating falsy values as absent
Fix: choose the check from the value domain. Use ?? when only nullish values mean absence; use has() when key presence itself matters, and test absence, undefined, zero, and false separately.
Expecting Set to deduplicate object content
Fix: to deduplicate by a stable business key, use a Map from that key to the object you want to retain, and decide explicitly whether first or last wins. Don’t treat generic JSON.stringify() as deep equality without a contract.
Deleting and reinserting during iteration
Fix: iterate a snapshot, or collect reorder intentions and apply them after the loop. If you only need to update an existing Map value, call set() directly without deleting the key first.
Serializing collections directly to JSON
Fix: define a wire format first. A string-keyed record can use Object.fromEntries(); when key types and order matter, encode a Map as an entry array and a Set as a value array, validate on input, then rebuild the collection.
Treating a weak collection as an observable cache
Fix: use an ordinary Map with an explicit policy when you need capacity, eviction, metrics, or enumeration. Use WeakMap only to query attached data through a weak key you still hold; resources still need an explicit close(), unsubscribe function, or finally.
Equality, iteration, and weak reachability
The specified complexity guarantee
ECMAScript requires Map and Set implementations to provide access times that are sublinear on average, but it neither requires a hash table nor promises O(1) for every operation. An engine may use a hash table, tree, or another structure that meets the observable semantics and complexity requirement. Application code can’t depend on bucket counts, hash values, or resize timing.
That guarantee rules out implementing every membership lookup as a full collection scan, but it doesn’t replace a benchmark. Converting an array to a Set has a construction cost, so one lookup may not justify the conversion. The model becomes natural when the same member set serves repeated queries. Any performance conclusion must name the target engine, data size, key type, and read-write mix.
Order state transitions
Insertion order belongs to entry state, not to a permanent timestamp on the key. The following transitions apply to Map keys; Set members follow the same rules.
| Operation | Changes size | Changes position |
|---|---|---|
| Insert a new key | Yes | Appends at the end |
| Update an existing key | No | No |
| Add an existing member | No | No |
| Delete an existing key | Yes | Removes the position |
| Insert after deletion | Yes | Appends at the end |
Insert after clear() | Yes | Starts a new order |
Live traversal makes collection changes affect an unfinished iterator immediately. If you update an unvisited key’s value, the iterator observes the new value; if you delete the key first, it doesn’t observe the key. New entries added before traversal ends may be visited.
This behavior can implement a work queue, but ordinary business loops usually need stable input. If new entries belong to the next pass, copy keys or entries at the start. When the data is too large to copy, maintain an explicit batch boundary instead of letting live iterator semantics define the scheduling policy implicitly.
Set-like arguments and result order
Set composition methods consume the right-hand argument through size, has(), and keys(), not through its default iterator. That design lets a Map participate as a set of keys and explains why an array doesn’t qualify. The three members on a custom set-like object must agree with each other or the result has no reliable meaning.
intersection() may choose the smaller side for iteration based on their sizes, so don’t treat its result order as a filtered copy of the left set. For example, if the left side is a, b, c and the smaller right side is c, a, Node 24 returns the intersection in c, a order. Sort explicitly after the set operation when output needs domain ordering.
Relation methods can return early from a size comparison or once they find enough evidence. Don’t put side effects in a set-like object’s has() or keys() methods. The specification’s allowed call order is part of the protocol contract, not a business event stream.
Why weak keys aren’t enumerable
A weak key expresses conditional reachability: the collection itself doesn’t keep the key alive, but while the program can still obtain the key, it can use it to retrieve the associated value. A WeakMap value can be any value and remains retrievable through a reachable key. Whether and when collection occurs belongs to the engine’s garbage collector.
If a weak collection exposed a key list or size, the same code could produce different results after an unpredictable garbage collection. Worse, enumeration would reacquire objects that were about to become unreachable. Removing those observation APIs lets an engine collect keys without changing visible program logic.
A non-registered Symbol can’t be recovered from the global registry, so the current specification permits it as a weak key. The result of Symbol.for(name) is retained by the global symbol registry and isn’t a garbage-collectable key; passing it to WeakMap.set() or WeakSet.add() throws TypeError.
Weak collections don’t create a security boundary. Code holding both the WeakMap and its key can still read the value, and code holding the object may expose data through another path. Prefer private fields for state owned by one class; WeakMap fits metadata owned outside the object’s implementation or shared across object types.
Finally, weak reachability isn’t resource management. File handles, subscriptions, locks, and transactions need release in deterministic control flow. Garbage collection only manages memory reachability and can’t promise that an external system observes cleanup before a deadline.
Further reading
4 questions · 1 predict-the-output · 1 spot-the-bug