The seventh primitive
ES2015 added a new primitive type to JavaScript: the Symbol. Every symbol is guaranteed unique, which makes symbols ideal as collision-proof property keys and as special hooks that customize how the language treats your objects. Symbols are one of the more mysterious corners of JavaScript — rarely used directly in app code, but powering iteration, coercion, and other behaviors under the hood. Understanding them rounds out your grasp of the object model and unlocks metaprogramming techniques.
Creating unique symbols
You make a symbol by calling Symbol(). Each call produces a brand-new, unique value — even
two symbols with the same description are different.
const a = Symbol()
const b = Symbol('id') // optional description (debug label only)
a === b // false
Symbol('id') === Symbol('id') // false always unique
typeof a // 'symbol'
The description is purely for debugging — it does not affect identity. Also note Symbol is
not a constructor: new Symbol() throws, because symbols are primitives, not objects.
Symbols as property keys
A symbol can be used as an object key via computed property syntax. Because it's unique, the key can never collide with any string key or another library's symbol.
const id = Symbol('id')
const user = { [id]: 123, name: 'Ada' }
user[id] // 123
user.id // undefined — the string 'id' is a different key
This makes symbols perfect for attaching metadata to objects you don't own (or that mix data from many sources) without risk of overwriting existing properties.
Hidden from enumeration
Symbol-keyed properties are skipped by for...in, Object.keys, Object.values,
Object.entries, and JSON.stringify. They're effectively invisible to ordinary enumeration
and serialization.
const secret = Symbol('secret')
const obj = { [secret]: 42, visible: 1 }
Object.keys(obj) // ['visible'] — symbol omitted
JSON.stringify(obj) // '{"visible":1}'
To retrieve symbol keys, you need Object.getOwnPropertySymbols(obj) or Reflect.ownKeys(obj)
(which returns both string and symbol keys). So symbols give "soft" privacy — hidden from casual
enumeration, but discoverable by code that looks specifically for them.
The global registry: Symbol.for
Sometimes you want the same symbol shared across modules or even realms (iframes, workers).
Symbol.for(key) looks up or creates a symbol in a process-wide global registry by
string key, returning the same symbol every time.
Symbol.for('app.id') === Symbol.for('app.id') // true shared by key
Symbol('app.id') === Symbol('app.id') // false — local, unique
Symbol.keyFor(Symbol.for('app.id')) // 'app.id' — recover the key
Use plain Symbol() for guaranteed-unique local keys, and Symbol.for when different parts of
a system must agree on one symbol. Registered symbols live for the realm's lifetime and aren't
garbage-collected, so don't over-populate the registry.
Well-known symbols
The most important use of symbols is the set of well-known symbols — built-in symbols
exposed as static properties on Symbol that let you hook into language operations.
Implementing the right one customizes how your object behaves with core syntax.
Symbol.iterator // makes an object iterable (for...of, spread)
Symbol.asyncIterator // for await...of
Symbol.toPrimitive // controls type coercion
Symbol.toStringTag // customizes Object.prototype.toString
Symbol.hasInstance // customizes instanceof
These are the connective tissue between your objects and the language's built-in behaviors.
Symbol.iterator — making objects iterable
Defining [Symbol.iterator]() (often as a generator method) makes your object work with
for...of, spread, and destructuring.
const range = {
*[Symbol.iterator]() {
yield 1; yield 2; yield 3
}
}
[...range] // [1, 2, 3]
for (const n of range) {} // works
Every built-in iterable (Array, Map, Set, String) implements this symbol — it is the iteration protocol, and symbols are what keep it collision-free.
Symbol.toPrimitive — controlling coercion
Defining [Symbol.toPrimitive](hint) lets an object decide how it converts to a primitive. The
hint is 'number', 'string', or 'default' depending on context.
const money = {
amount: 5,
[Symbol.toPrimitive](hint) {
return hint === 'string' ? `$${this.amount}` : this.amount
}
}
`${money}` // '$5' (string hint)
money * 2 // 10 (number hint)
This single hook overrides the default valueOf/toString dance with precise control over
coercion in different contexts.
Symbols and private-ish fields
Because symbol keys are hidden from enumeration and serialization, they're sometimes used for
"internal" properties. But this is soft privacy — anyone with the symbol or via
Object.getOwnPropertySymbols can still access them.
const _balance = Symbol('balance')
class Account {
constructor() { this[_balance] = 0 } // hidden from JSON/keys, but not truly private
}
For hard privacy, use class #private fields, which are genuinely inaccessible from outside.
Symbols are for collision-avoidance, not security.
No implicit string coercion
Symbols deliberately don't auto-coerce to strings, to avoid silent bugs. Implicit conversion (concatenation, template slots) throws; you must convert explicitly.
const s = Symbol('id')
'' + s // TypeError
`${s}` // TypeError
String(s) // 'Symbol(id)' explicit
s.toString() // 'Symbol(id)'
s.description // 'id'
This strictness is intentional — it surfaces accidental symbol-to-string conversions instead of producing nonsense.
When to use symbols
Reach for symbols when you need collision-free keys for metadata (especially on objects you
don't control), enum-like unique constants that can't be matched by an equal string literal,
or when you want to hook into language features via well-known symbols. For serializable
values that cross network/storage boundaries, prefer strings — symbols don't survive
JSON.stringify. And for true encapsulation, prefer #private fields.
Key takeaways
- A Symbol is a unique primitive (
typeofis'symbol');new Symbol()throws. The description is a debug label only. - Symbol keys are collision-free and hidden from
for...in/Object.keys/JSON.stringify— retrieve them withObject.getOwnPropertySymbolsorReflect.ownKeys. Symbol.for(key)shares a symbol via the global registry; plainSymbol()is locally unique.- Well-known symbols (
iterator,asyncIterator,toPrimitive,toStringTag,hasInstance) hook into core language behavior. - Symbols give soft privacy; use
#privatefields for real encapsulation. - Symbols never implicitly stringify — convert with
String(sym); they don't survive JSON.
Symbols are the quiet machinery behind iteration, coercion, and collision-free keys — knowing them turns "magic" language behavior into something you can customize deliberately.