What Is a Symbol in JavaScript? Well-Known Symbols
A JavaScript symbol is a unique primitive used as a collision-free property key. Symbol vs Symbol.for, what skips symbol keys, and the well-known symbols.
- Course: JavaScript study plan
- Module: Collections and iteration
- Kind: Lesson
- Reading time: 11 min
- Runtime: Node 16
What is a Symbol in JavaScript?
A symbol is a JavaScript primitive whose only job is to be unique — Symbol("id") === Symbol("id") is false. Used as a property key it cannot collide with any other key, and it is skipped by Object.keys, for…in and JSON.stringify. The language also uses well-known symbols such as Symbol.iterator as hooks that your objects can implement.
Lesson
A symbol is a primitive whose only job is to be unique: Symbol("id") !== Symbol("id"). Used as a property key it cannot collide with any string key or any other symbol, it is skipped by Object.keys, for…in and JSON.stringify, and it lets two pieces of code attach data to the same object without knowing about each other. The language itself uses a fixed set of well-known symbols as hook names — Symbol.iterator is the one you have been using all module — so that your objects can plug into for…of, instanceof, string conversion and more without the language reserving any string names. This lesson covers the primitive, the global registry, symbols as keys, and the well-known symbols worth knowing by name.
The primitive
const id = Symbol("user id"); // the string is a description for debugging only
typeof id; // "symbol"
id.description; // "user id"
id.toString(); // "Symbol(user id)"
String(id); // same; `${id}` and id + "" THROW — no implicit string conversion
Symbol("x") === Symbol("x"); // false — every call is a new symbol
Symbols cannot be newed (new Symbol() throws), are never coerced to strings implicitly (a guard against accidentally turning a key into "Symbol(x)"), and are compared by identity. The seventh primitive type, after string, number, bigint, boolean, undefined, null.
Symbols as property keys
const HIDDEN = Symbol("hidden");
const obj = { visible: 1, [HIDDEN]: "secret" }; // computed key syntax
obj[HIDDEN]; // "secret" — must use brackets; obj.HIDDEN is the string key "HIDDEN"
Object.keys(obj); // ["visible"]
JSON.stringify(obj); // {"visible":1}
for (const k in obj) { } // visible only
Object.getOwnPropertySymbols(obj); // [Symbol(hidden)]
Reflect.ownKeys(obj); // ["visible", Symbol(hidden)] — everything, strings first
{ ...obj } // copies symbol keys too (own enumerable, any type)
Object.assign({}, obj) // same
Symbol-keyed properties are not private — anyone with Object.getOwnPropertySymbols or the symbol itself can read them. They are collision-free and invisible to the common enumeration paths, which is a different, useful guarantee: a library can stamp obj[Symbol("cached")] on your objects without ever clashing with your fields or leaking into your JSON. For true privacy, #fields or a WeakMap.
The global registry: Symbol.for
Symbol.for("app.id") === Symbol.for("app.id"); // true — looked up (or created) in a process-wide registry
Symbol.keyFor(Symbol.for("app.id")); // "app.id"
Symbol.keyFor(Symbol("local")); // undefined — not registered
Symbol.for(key) returns the same symbol for the same string everywhere in the realm — across modules, across bundles, across iframes. Use it when two independently loaded pieces of code must agree on a key (Node's own Symbol.for("nodejs.util.inspect.custom") is the canonical example). Use plain Symbol() when uniqueness is the point.
Well-known symbols
Built-in symbols stored as static properties of Symbol; the language looks them up on your objects at specific moments:
| Symbol | The language calls it when… | You implement |
|---|---|---|
Symbol.iterator | for…of, spread, destructuring, Array.from | a method returning an iterator (usually a generator) |
Symbol.asyncIterator | for await…of | an async generator or async iterator |
Symbol.toPrimitive | conversion to a primitive (+, ${}, ==) | (hint) => primitive — overrides valueOf/toString |
Symbol.toStringTag | Object.prototype.toString.call(x) | a getter returning the tag, giving [object Tag] |
Symbol.hasInstance | x instanceof C | static [Symbol.hasInstance](x) — custom instanceof |
Symbol.species | array/promise methods creating derived objects | a static getter returning the constructor to use |
Symbol.isConcatSpreadable | arr.concat(x) | true to spread an array-like, false to keep an array whole |
Symbol.match/replace/search/split | string methods given a non-RegExp | objects that act like patterns |
Symbol.unscopables | with statements | ignore |
class Even {
static [Symbol.hasInstance](n) { return Number.isInteger(n) && n % 2 === 0; }
}
4 instanceof Even; // true — no Even object was ever created
const arrayLike = { length: 2, 0: "a", 1: "b", [Symbol.isConcatSpreadable]: true };
[1].concat(arrayLike); // [1, "a", "b"]
You will implement Symbol.iterator often, Symbol.toPrimitive/toStringTag sometimes, hasInstance and species almost never — but recognising them explains behaviour that otherwise looks magical (why Stack extends Array methods return Stacks: species).
Enums and sentinels
Symbols make excellent sentinel values — const MISSING = Symbol("missing") can never be confused with a real value the way null or -1 can — and readable enums (const Color = Object.freeze({ Red: Symbol("red"), Green: Symbol("green") })) whose members cannot be forged from strings. The cost: symbols do not serialise, so anything that crosses JSON needs a string mapping.
Common mistakes
obj.SYMinstead ofobj[SYM](creates a string-keyed property).- Expecting
JSON.stringifyorObject.keysto show symbol keys — or, the reverse, expecting symbol keys to be private. - `
${sym}— throws; usesym.descriptionorString(sym)`. - Using
Symbol()where two modules need the same key (useSymbol.for), orSymbol.forwhere uniqueness matters. - Storing symbols in something that will be serialised.
Interview angle
- "What is a symbol for?" A unique, collision-free property key hidden from ordinary enumeration; and the language's hook names (well-known symbols).
- "
Symbol()versusSymbol.for()?" Fresh and unique every call versus one shared symbol per string in a global registry. - "Are symbol properties private?" No — hidden from
keys/JSON/for…in, visible viagetOwnPropertySymbols/Reflect.ownKeys. - "Name three well-known symbols."
iterator,toPrimitive,hasInstance(ortoStringTag,asyncIterator,species). - "How would you customise
instanceof?"static [Symbol.hasInstance](x)on the class.
Key takeaways
Symbol(desc)is unique and never implicitly stringified;typeofis"symbol".- As keys: brackets to access; skipped by
keys/for…in/JSON; found bygetOwnPropertySymbols/Reflect.ownKeys; copied by spread. Symbol.forshares one symbol per string across the realm;Symbol.keyForreads it back.- Well-known symbols are hooks:
iterator,asyncIterator,toPrimitive,toStringTag,hasInstance,species,isConcatSpreadable. - Symbols are ideal sentinels and enum members, and do not survive JSON.
Common questions
What is the difference between Symbol() and Symbol.for()?
Symbol() creates a new, unique symbol on every call. Symbol.for(key) looks the key up in a global registry and returns the same symbol for the same string everywhere in the realm, across modules and bundles; Symbol.keyFor reads the key back from a registered symbol.
Are symbol properties private in JavaScript?
No. Symbol-keyed properties are skipped by Object.keys, for…in and JSON.stringify, but Object.getOwnPropertySymbols and Reflect.ownKeys list them, and spread copies them. They are collision-free, not private; use #private fields or a WeakMap for real privacy.
What are well-known symbols in JavaScript?
Well-known symbols are built-in symbols stored on Symbol that the language looks up on your objects at set moments: Symbol.iterator for for…of and spread, Symbol.asyncIterator for for await, Symbol.toPrimitive for conversion, Symbol.hasInstance for instanceof, plus Symbol.toStringTag, Symbol.species and others.
Why does converting a symbol to a string throw?
Symbols are never converted to strings implicitly, so a template literal like ${sym} or sym + "" throws a TypeError. That guards against a key silently turning into the text "Symbol(x)". Convert explicitly with String(sym), sym.toString() or sym.description.
Why use a symbol as a sentinel value?
A sentinel such as const MISSING = Symbol("missing") can never be confused with real data the way null or -1 can, and enum members made of symbols cannot be forged from strings. The cost is that symbols do not survive JSON.stringify, so anything crossing JSON needs a string mapping.
Exercises
String keys and symbol keys, side by side
Line 1 lists string keys (assign each its index), line 2 lists names for Symbol(name) keys (assign hidden-<i>). Build one object with both. Print keys=<Object.keys> json=<JSON.stringify>; symbols=<getOwnPropertySymbols count> descriptions=<their descriptions>; ownKeys=<Reflect.ownKeys length> spreadKeepsSymbols=<does { ...obj } keep them all>; then for the first symbol name: plainEqual=<Symbol(n) === Symbol(n)> forEqual=<Symbol.for(n) === Symbol.for(n)> keyFor=<Symbol.keyFor(Symbol.for(n))> keyForPlain=<Symbol.keyFor of your first plain symbol>; finally try to put the first symbol in a template literal and print template=<result or the error's constructor name> string=<String(sym)> typeof=<typeof sym>.
Example: name born / id secret →
keys=name,born json={"name":0,"born":1}
symbols=2 descriptions=id,secret
ownKeys=4 spreadKeepsSymbols=true
plainEqual=false forEqual=true keyFor=id keyForPlain=undefined
template=TypeError string=Symbol(id) typeof=symbolWell-known symbols in action
Implement three hooks. Even gets static [Symbol.hasInstance](n) so 4 instanceof Even is true for even integers. Version(major, minor) gets [Symbol.toPrimitive](hint) — "number" → major + minor / 100, otherwise v<major>.<minor> — and get [Symbol.toStringTag]() returning "Version". Line 1 is numbers, line 2 a version like 1.4. Print even=<comma list> odd=<comma list>; then string=<template of v> number=<+v> default=<v + ""> compare=<v > 1> tag=<Object.prototype.toString.call(v)>; then build { length: 2, 0: "x", 1: "y", [Symbol.isConcatSpreadable]: true } and the same object without the symbol and print concatSpreadable=<JSON of [1].concat(spreadable)> concatPlain=<JSON of [1].concat(plain)>.
Example: 1 2 3 4 10 7 / 1.4 →
even=2,4,10 odd=1,3,7
string=v1.4 number=1.04 default=v1.4 compare=true tag=[object Version]
concatSpreadable=[1,"x","y"] concatPlain=[1,{"0":"x","1":"y","length":2}]In this module: Collections and iteration
- Map and Set — real keyed collections
- WeakMap, WeakSet and WeakRef — data that does not keep objects alive
- The iteration protocol — what for…of, spread and destructuring actually call
- Generators — functions that pause, and lazy pipelines
- Symbols — unique keys and the language's extension points (this lesson)
- Checkpoint — Collections and iteration
← Generators — functions that pause, and lazy pipelines · Checkpoint — Collections and iteration →