skip to content

Symbols and Well-Known Symbols

Symbols exist so you can attach a property nobody else can collide with or accidentally enumerate. In interviews the well-known symbols matter more: they are the documented hooks for customizing iteration, coercion, and instanceof.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

questions

5

In JavaScript, what is a Symbol, and what does using a symbol as an object property key give you that a string key does not?

level: juniorimportance: must knowfreq 58%

answer

  1. a primitive, not an object
  2. every call makes a new value
  3. description is only a label
  4. collision-proof property keys
  5. hidden from loops, not from getOwnPropertySymbols

basics

~20 s

A Symbol is a primitive whose every value is unique: Symbol('id') never equals another Symbol('id'). Used as a property key, it cannot collide with any string key or with symbols created by other code, even when the descriptions match.

solid answer

~40 s

`Symbol` is one of JavaScript's primitive types, created by calling `Symbol()` as a function with an optional description string. Every call returns a brand-new value, so `Symbol('id') === Symbol('id')` is `false`, and `typeof` on one returns `'symbol'`. `Symbol` is not a constructor — `new Symbol()` throws a `TypeError`. Because each symbol is unique, a symbol property key can never clash with a string key or with a symbol another library made, which is why you use one to attach metadata to an object you do not own. You read the description back with `sym.description` (added in ES2019) and write the key with computed-key syntax: `{ [ID]: 1 }` and `obj[ID]`. Symbols hide a property from ordinary enumeration, but they are not access control — `Object.getOwnPropertySymbols` still finds them.

go deeper

for a junior

Be able to say that a symbol is a unique primitive, that Symbol('a') === Symbol('a') is false, and that a symbol used as a property key cannot collide with anyone else's key.

for a middle

Explain the mechanics: Symbol is not a constructor, keys must be written with computed-key brackets, and symbol keys are skipped by Object.keys, for...in and JSON.stringify but returned by Object.getOwnPropertySymbols.

for a senior

Show judgment about when a symbol key is the right tool — annotating objects you do not own, keeping framework bookkeeping out of user-visible loops — and warn clearly that it buys collision-safety, not confidentiality or serialisation safety.

for a principal

Own the API-design angle: choosing between a symbol key, a WeakMap side table and a documented string key changes who can discover the data, whether it survives cloning and serialisation, and how a library's internals leak into consumers' object graphs.

## What a symbol actually is A *symbol* is a primitive value, sitting alongside string, number, boolean, `null`, `undefined` and bigint in the language's set of primitives. You create one by calling the global `Symbol` function, optionally passing a **description** — a human-readable label used only for debugging. ```js const a = Symbol('id'); const b = Symbol('id'); a === b; // false — every call makes a new value typeof a; // 'symbol' a.description; // 'id' (ES2019 accessor) a.toString(); // 'Symbol(id)' ``` The description plays no role in equality. Two symbols are the same value only when they are literally the same symbol — the one produced by one particular call. That is the whole point: a symbol is a value you can hand out, and nobody can forge another one that compares equal to it. ## Symbol is a function, not a constructor ```js new Symbol('id'); // TypeError: Symbol is not a constructor ``` The spec deliberately blocks `new` here. Wrapper objects around primitives are already a source of confusion, and a `Symbol` object that is truthy but not `===` to the primitive it wraps would be worse. If you ever genuinely need the wrapper (rarely), `Object(sym)` produces it. ## Why a unique key is useful Property keys in JavaScript are strings or symbols — nothing else. (A number key like `obj[1]` is converted to the string `'1'`.) With string keys, any two pieces of code that pick the same name write to the same slot: ```js // library A obj.id = 42; // library B, later, in the same object obj.id = 'user-7'; // silently clobbers A ``` With a symbol, each library holds its own key and neither can see or overwrite the other's: ```js const ID = Symbol('id'); // module-private value let counter = 0; export function tag(obj) { obj[ID] = ++counter; } export function idOf(obj) { return obj[ID]; } ``` This is the everyday use: annotating objects you do not own — DOM-free plain objects passed through a framework, instances from a third-party class, an options bag threaded through several layers — without adding a name anyone else might also pick. ## Syntax: symbol keys are always computed Because the key is a *value*, not an identifier, you cannot write `obj.ID`. Every use goes through brackets, including in object literals and class bodies: ```js const ID = Symbol('id'); const user = { [ID]: 1, name: 'Ada' }; user[ID]; // 1 user.ID; // undefined — this is the string key 'ID' class Repo { [ID]() { return 'method with a symbol key'; } } ``` That also means you must keep the symbol itself around. A symbol you created and then dropped on the floor makes its property effectively unreachable — which is occasionally a feature and more often a bug. ## Hidden from enumeration, but not private Symbol-keyed properties are skipped by `for...in`, `Object.keys`, `Object.values`, `Object.entries`, `Object.getOwnPropertyNames` and `JSON.stringify`. That keeps your annotation out of everybody's loops and serialised payloads. It is *not* an access-control mechanism: `Object.getOwnPropertySymbols(obj)` returns the symbol keys, and `Reflect.ownKeys(obj)` returns strings and symbols together. Anyone with a reference to the object can enumerate and read them. ```js const s = Symbol('secret'); const o = { [s]: 'value', open: 1 }; Object.keys(o); // ['open'] JSON.stringify(o); // '{"open":1}' Object.getOwnPropertySymbols(o); // [ Symbol(secret) ] ``` If you want a member outsiders genuinely cannot reach, symbols are the wrong tool; they solve *collision*, not *confidentiality*. ## Two flavours beyond plain Symbol() The language adds two structured sources of symbols on top of anonymous `Symbol()` values. `Symbol.for(key)` looks a symbol up in a process-wide registry, returning the same symbol for the same string every time — the opposite of uniqueness, chosen when two independent copies of code must agree on one key. And the *well-known symbols* — `Symbol.iterator`, `Symbol.asyncIterator`, `Symbol.toPrimitive`, `Symbol.toStringTag`, `Symbol.hasInstance`, `Symbol.species` and a handful more — are fixed symbols the specification itself looks up on your objects, so that defining one lets your type plug into a built-in operation. ## Cost and gotchas Symbols are cheap; they are ordinary primitive values. The practical gotchas are: they vanish from JSON, so anything you need to survive serialisation must live under a string key; they cannot be implicitly concatenated into a string (`'x' + sym` throws); and `Object.assign` and object spread *do* copy own enumerable symbol keys, so a symbol annotation survives a shallow clone even though it disappeared from `Object.keys`.

  • If two symbols with the same description are never equal, what is the description actually for?
    Debugging only. It shows up in `sym.toString()` as `Symbol(id)`, in devtools output, and via the `sym.description` accessor added in ES2019. It takes no part in equality and no part in property lookup — two symbols made with the same description are still different keys. Treat it like a variable name you can read at runtime.
  • Can a symbol ever be coerced to a string implicitly?
    No. Implicit string coercion of a symbol throws a `TypeError`, so `'id: ' + sym` and `` `${sym}` `` both fail. You must ask explicitly: `String(sym)` and `sym.toString()` both return `'Symbol(id)'`. The spec made this loud on purpose, because a silently stringified symbol would quietly become an ordinary string key.
  • Do symbol-keyed properties survive Object.assign or object spread?
    Yes. Both copy own *enumerable* properties, and that includes symbol keys — `{ ...obj }` carries your symbol annotation into the clone. This surprises people who assume that anything invisible to `Object.keys` is also invisible to spread. What does not survive is `JSON.parse(JSON.stringify(obj))`, since JSON has no notion of symbols at all.

saying these in an interview costs you the question

  • Says two symbols with the same description are equal
  • Calls new Symbol('id') to create one
  • Claims symbol properties are private and unreachable
  • Expects symbol-keyed data to survive JSON.stringify
  • Thinks symbols are objects rather than primitives

context

open as a page

Which JavaScript operations skip symbol-keyed properties on an object, and which ones still expose or copy them?

level: middleimportance: should knowfreq 44%

basics

~10 s

Symbol keys are skipped by for...in, Object.keys/values/entries, Object.getOwnPropertyNames and JSON.stringify. They are still returned by Object.getOwnPropertySymbols and Reflect.ownKeys, and copied by Object.assign and object spread. Hidden from loops, not private.

open as a page

Why does `const s = Symbol('id'); console.log('id: ' + s);` throw in JavaScript, while `String(s)` returns a string?

level: middleimportance: should knowfreq 38%

basics

~20 s

Implicit coercion of a symbol to a string throws a TypeError by design, so a symbol can never silently become a string property key. String(s) and s.toString() are explicit requests, and both return the string 'Symbol(id)'.

open as a page

JavaScript's well-known symbols, such as `Symbol.iterator` and `Symbol.toStringTag`, key the language's built-in extension hooks. Why did the specification use symbol keys for these hooks instead of ordinary string names?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Symbol keys let the specification add new hooks that no existing code can already have defined. A string name like 'iterator' might collide with a property some object already uses, silently changing its behaviour; a fresh symbol cannot collide, so opting in is always deliberate.

open as a page

What is the difference between `Symbol('app.id')` and `Symbol.for('app.id')` in JavaScript?

level: middleimportance: nice to knowfreq 33%

basics

~20 s

Symbol('app.id') creates a brand-new unique symbol every call. Symbol.for('app.id') looks the string up in a global registry shared across realms and returns the same symbol every time, so independent code can agree on one key.

open as a page