A Symbol Is a Guaranteed-Unique Value
Symbol is one of JavaScript's primitive types, alongside string, number, boolean, null, undefined, and bigint. Its defining property is simple: every symbol you create is different from every other symbol, forever.
Two symbols, both created with no arguments, and they're still not equal. That's not an accident — it's the whole point. You can't forge a symbol, accidentally recreate one, or collide with someone else's.
You can attach a description to help with debugging. It doesn't affect identity:
Same description, different symbols. The description is purely a label for humans reading logs.
Why They Exist: Collision-Free Keys
The main reason symbols exist is to let you add properties to an object without worrying that your key clashes with someone else's. String keys compete in a flat namespace — if two libraries both decide to stash metadata at obj.meta, they stomp on each other. Symbol keys don't compete, because nobody else has your symbol.
The square brackets in [ID]: 42 are computed-property syntax — they say "use the value of ID as the key." Only code that holds the same ID symbol can read or overwrite that property. Any other module using the string "id" or its own Symbol("id") is talking about a completely different slot.
Symbol Keys Are (Mostly) Hidden
Symbol-keyed properties don't show up in for...in, Object.keys, or JSON.stringify. They're not secret — determined code can still find them — but they stay out of the way during normal iteration.
Object.keys and JSON.stringify skip the symbol key entirely. If you need to find symbol properties deliberately, Object.getOwnPropertySymbols and Reflect.ownKeys expose them. This is exactly the behavior you want for metadata — visible when you go looking, invisible to code that's just walking string keys.
Sharing Symbols With Symbol.for
Symbol() gives you a local, one-off symbol. Sometimes you want the opposite — a symbol that's the same everywhere in the program, across modules, so different pieces of code can agree on a key. That's what Symbol.for is for.
Symbol.for(key) checks a global registry: if a symbol for that key already exists, you get it back; otherwise it creates one and stores it. Symbol.keyFor goes the other direction — it returns the key a registered symbol was created with (or undefined for non-registered ones).
Most of the time, plain Symbol() is what you want. Reach for Symbol.for when a symbol really needs to be shared across module boundaries.
Well-Known Symbols: Hooks Into the Language
Here's where symbols earn their keep. JavaScript exposes a set of predefined symbols — the well-known symbols — that the language itself checks for on your objects. Implement a method at one of these keys, and your object plugs straight into a built-in feature.
The most useful one is Symbol.iterator. Any object that has a method at this key is iterable: it works with for...of, spread, destructuring, and Array.from.
range isn't an array. It's a plain object with one specially-keyed method. But because that method returns an iterator, for...of and spread both work on it — the same way they work on arrays, strings, and Map. That's the contract: implement Symbol.iterator, and the language treats you as iterable.
Other Well-Known Symbols Worth Knowing
There are a handful more, each one a hook into a different part of the language:
Symbol.iterator— makes an object iterable.Symbol.asyncIterator— same, but forfor await...of.Symbol.toPrimitive— controls how an object converts to a primitive (number, string, or default) in coercion contexts.Symbol.hasInstance— customizes whatinstanceofreturns for your class.Symbol.toStringTag— sets the tag shown inObject.prototype.toString.call(obj).
You don't need to memorize them. Knowing they exist is enough — when you hit a situation where you want your object to behave like a built-in type, there's usually a well-known symbol for it.
Symbols Are Not Strings
A common trip-up: symbols don't auto-convert to strings. Concatenating one throws, and so does feeding one to an API that expects a string:
Template literals throw the same TypeError. Use String(symbol) or symbol.toString() when you actually want text. The rough edges are intentional — the language is protecting you from accidentally treating a unique identity value as just another piece of string data.
When to Use Symbols (and When Not To)
Reach for symbols when:
- You're attaching metadata to objects you don't own, and need a key that won't clash.
- You're designing a protocol — "objects that want to work with my library should implement a method at this symbol."
- You want a property that doesn't leak into
JSON.stringifyorfor...in. - You're implementing
Symbol.iteratoror another well-known symbol.
Skip symbols when:
- You just need an object key and there's no collision risk. A string is simpler and shows up in logs as itself.
- You want "private" fields. Symbol-keyed properties aren't truly private —
Object.getOwnPropertySymbolsfinds them. For actual privacy, use#privateclass fields. - You're storing data that needs to survive
JSON.stringify. It won't.
Most JavaScript code gets by fine without ever writing Symbol(...) directly. But the second you want your own object to work with for...of or behave naturally in a coercion context, symbols are the door into that machinery.
Next: Function Declarations
Symbols, iterators, and generators all lean heavily on functions — the methods stored at Symbol.iterator, the factories that return iterators, the generators whose function* form hides most of the boilerplate. Functions are the next chapter, starting with the different ways to declare them and what changes between each form.
Frequently Asked Questions
What is a symbol in JavaScript?
A symbol is a primitive type whose values are guaranteed unique. You create one with Symbol() or Symbol('description'), and no two calls ever produce equal symbols. They're mostly used as object keys that won't collide with other keys, and as hooks into language features via well-known symbols like Symbol.iterator.
When should I use a symbol instead of a string key?
Use a symbol when you want to attach a property to an object without risking a name collision with other code — libraries adding metadata, frameworks tagging objects, or defining a key that's intentionally not part of the public API. For everyday keys where readability matters and collisions aren't a concern, strings are still the right choice.
What is Symbol.iterator used for?
Symbol.iterator is a well-known symbol that tells for...of, spread, and destructuring how to iterate an object. If you define a method at the Symbol.iterator key that returns an iterator, your object becomes iterable. That's how arrays, strings, Map, and Set all work under the hood.
What's the difference between Symbol() and Symbol.for()?
Symbol('x') creates a brand-new symbol every time — two calls with the same description are still different. Symbol.for('x') looks up a global registry and returns the same symbol for the same key across the whole program. Use Symbol.for when you need symbols to be shared across modules or realms; use Symbol() for local uniqueness.