Free lesson · JavaScript Basic to Advance
Symbols and Well-Known Symbols
A Primitive You Probably Won't Use Often
JavaScript has seven primitive types — string, number, boolean, null, undefined, bigint, and symbol. The first five appeared in Module 2. Symbols are the odd one out. They were added in ES6 to solve specific advanced problems, and most application code never uses them directly. But they appear in framework internals, in some standard library features, and in interview questions about JavaScript depth.
// Create a symbol
const id = Symbol("id");
console.log(typeof id); // "symbol"
console.log(id.toString()); // "Symbol(id)"
// Every symbol is unique, even with the same description
const a = Symbol("description");
const b = Symbol("description");
console.log(a === b); // false — different symbols
A symbol is a unique, opaque value. The description (the string you pass to Symbol(...)) is for debugging only — it doesn't affect the symbol's identity. Two symbols with the same description are still different symbols. This uniqueness is the whole point.
This Topic covers what symbols are, the few places they're useful, and the well-known symbols that enable some of JavaScript's most-used features.
Interactive animation: Same description, different symbols - every Symbol() call creates a new value, so the two never compare equal.
Why Symbols Exist
The problem symbols solve: how do you add a property to an object without risking collision with someone else's property name?
// Pre-symbol approach — string key, possible collision
const user = { name: "Olena" };
user.metaId = 12345; // what if some library also adds .metaId?
// With symbols — guaranteed unique key
const metaIdKey = Symbol("metaId");
user[metaIdKey] = 67890;
console.log(user.metaId); // 12345
console.log(user[metaIdKey]); // 67890
console.log(Object.keys(user)); // ["name", "metaId"] — symbol key hidden
The symbol key is invisible to most enumeration. Object.keys, for...in, JSON.stringify, and most other ways of inspecting an object don't show symbol keys. The only way to find them is Object.getOwnPropertySymbols(obj).
This makes symbols useful as "private-ish" keys — not enforced privacy (Topic 8.5 covered real private fields with #), but a strong convention that says "this property is internal, don't iterate over it accidentally."
In practice, this is rarely needed in application code. Class private fields handle the privacy case. Map handles the "non-string keys" case. Symbols mostly appear in framework internals and in JavaScript's own standard library.
Interactive animation: Keys that cannot collide - a symbol key sits beside string keys and stays hidden from Object.keys.
Symbol.for: Global Symbol Registry
Sometimes you need symbols that can be shared across different parts of your code (or different scripts, or different modules). For this, JavaScript has a global symbol registry accessed through Symbol.for:
const a = Symbol.for("shared.id");
const b = Symbol.for("shared.id");
console.log(a === b); // true — same symbol from the registry
// Compare with plain Symbol()
const c = Symbol("shared.id");
const d = Symbol("shared.id");
console.log(c === d); // false — different symbols
Symbol.for(key) returns an existing symbol with that key from the registry, or creates a new one if none exists. Calling it again with the same key returns the same symbol. This is useful when libraries need to communicate via shared symbol keys.
Symbol.keyFor(sym) is the reverse — given a registered symbol, return its key:
const sym = Symbol.for("shared.id");
console.log(Symbol.keyFor(sym)); // "shared.id"
You rarely need the registry directly. But it's worth knowing it exists for advanced framework code.
Interactive animation: Shared identity, by agreement - Symbol.for returns the same symbol for the same key from the global registry.
Well-Known Symbols
The most useful application of symbols is the well-known symbols that JavaScript itself provides. These are special symbols whose presence on an object enables specific language features.
The most important is Symbol.iterator. Any object with a Symbol.iterator method becomes iterable — usable in for...of loops, spread syntax, and destructuring.
class Counter {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
}
const counter = new Counter(1, 5);
// Now it works with for...of
for (const num of counter) {
console.log(num);
}
// And with spread
console.log([...counter]);
// And with destructuring
const [first, second] = counter;
console.log(first, second);
The [Symbol.iterator]() method is what for...of calls under the hood. It must return an iterator object — something with a next() method that returns { value, done } on each call. This is the iteration protocol.
Arrays, strings, Sets, Maps, and Generators all implement this protocol — that's why they all work in for...of. Custom classes can implement it to gain the same support.
Other Well-Known Symbols
A quick tour of others you'll encounter:
Symbol.asyncIterator — for async iteration. The async equivalent of Symbol.iterator. We saw this in Topic 9.10 when we covered async generators.
Symbol.toPrimitive — controls how an object converts to a primitive (in coercion contexts like + or template literals).
class Money {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
}
[Symbol.toPrimitive](hint) {
if (hint === "number") return this.amount;
if (hint === "string") return `${this.amount} ${this.currency}`;
return `Money: ${this.amount} ${this.currency}`;
}
}
const m = new Money(100, "USD");
console.log(+m); // 100 (number hint)
console.log(`${m}`); // "100 USD" (string hint)
console.log(m + ""); // "Money: 100 USD" (default hint)
This is rarely useful in application code, but it shows the power of well-known symbols — they let you customize built-in language behaviors.
Symbol.hasInstance — controls how instanceof works.
Symbol.toStringTag — controls what Object.prototype.toString.call(obj) returns:
class Currency {
get [Symbol.toStringTag]() {
return "Currency";
}
}
const c = new Currency();
console.log(Object.prototype.toString.call(c)); // "[object Currency]"
These are deep-language features. You won't write code that uses them often. But understanding that JavaScript's built-in behaviors hook into well-known symbols is part of knowing the language at depth.
graph TD
A[Symbols] --> B[Symbol]
A --> C[Symbol.for]
A --> D[Well-known Symbols]
B --> E[Unique value<br/>not in global registry]
C --> F[Shared via global registry]
D --> G[Symbol.iterator<br/>Symbol.asyncIterator<br/>Symbol.toPrimitive<br/>Symbol.hasInstance<br/>Symbol.toStringTag]
style A fill:#7c3aed,color:#fff
style D fill:#10b981,color:#fff
This diagram summarizes the three uses of Symbol. The bare Symbol function creates a unique symbol value, not registered anywhere. Symbol.for uses the global symbol registry to create or retrieve shared symbols across code boundaries. Well-known symbols are special symbols defined by the JavaScript spec — Symbol.iterator for making objects iterable, Symbol.asyncIterator for async iteration, Symbol.toPrimitive for custom coercion, Symbol.hasInstance for custom instanceof behavior, and Symbol.toStringTag for custom toString output.
Interactive animation: Symbol.toStringTag and Symbol.hasInstance, live - two well-known symbols change what toString reports and how instanceof answers.
When You'll Actually Use Symbols
Realistically, in application code, you'll use symbols rarely. Three main cases:
Custom iterables. When a class represents a collection and you want it to work with for...of, spread, and destructuring, implement [Symbol.iterator]().
Library internals. If you're writing a library and need to attach metadata to user-supplied objects without risking collision, symbol keys are the safer choice than string keys.
Hooking into standard behavior. Very rarely, you'll want to customize how objects convert to primitives or how instanceof works. Well-known symbols are how.
For everything else — private state (Module 8 private fields), unique identifiers (just use a counter or UUID), non-string keys (use Map) — there are better tools. Symbols are real, useful, and well-known JavaScript features, but they aren't part of most developers' daily work.
Common Pitfalls
Warning: The symbol traps.
Symbols don't survive
JSON.stringify. Symbol keys and values are dropped from JSON serialization. If you need persistable data, don't use symbols as keys.Symbols don't appear in
Object.keysorfor...in. This is sometimes the feature, but if you're trying to inspect all of an object's properties, remember to also checkObject.getOwnPropertySymbols.
Symbol("desc")andSymbol("desc")are different. The description is for debugging only. To get a shared symbol, useSymbol.for.Don't use
new Symbol(...). Symbols are not constructable.Symbol()is a regular function call, not a constructor.
Why This Matters in Interviews
Interview: Symbol questions are rare in standard interviews and more common at senior or staff levels. When they appear, the question is usually conceptual: "What are symbols and what are they used for?" The expected answer covers uniqueness, the iteration protocol via
Symbol.iterator, and the role of well-known symbols in customizing language behavior.Implementation questions sometimes ask: "Make this class iterable" — the expected answer involves
[Symbol.iterator]().Topic 10.7 — Interview Lens: Modern JavaScript Patterns — covers the deeper treatment.
Try It
Exercise: Build a
Rangeclass that represents an integer range and is iterable:class Range { constructor(start, end, step = 1) { // your code } [Symbol.iterator]() { // your code } } const r = new Range(1, 10, 2); for (const n of r) console.log(n); // 1, 3, 5, 7, 9 console.log([...r]); // [1, 3, 5, 7, 9]By the time this works, you've implemented the iteration protocol from scratch. This pattern shows up in real libraries — pagination iterators, lazy sequences, custom data structures.