Menu

JavaScript Symbols: Unique Keys, Symbol.iterator, and Well-Known Symbols

What JavaScript symbols are, why they exist, and how well-known symbols like Symbol.iterator let your own objects plug into language features.

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.

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

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.

index.js
Output
Click Run to see the output here.

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.

index.js
Output
Click Run to see the output here.

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.

index.js
Output
Click Run to see the output here.

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.

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.
  • Symbol.iterator — makes an object iterable.
  • Symbol.asyncIterator — same, but for for await...of.
  • Symbol.toPrimitive — controls how an object converts to a primitive (number, string, or default) in coercion contexts.
  • Symbol.hasInstance — customizes what instanceof returns for your class.
  • Symbol.toStringTag — sets the tag shown in Object.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:

index.js
Output
Click Run to see the output here.

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.stringify or for...in.
  • You're implementing Symbol.iterator or 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.getOwnPropertySymbols finds them. For actual privacy, use #private class 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.

Learn to code with Coddy

GET STARTED