jsguides

Symbol

Symbol is a built-in object that creates a unique identifier. Symbols are often used as object property keys to avoid naming collisions, and they serve as the foundation for well-known symbols that customize JavaScript behavior.

Creating Symbols

Create a Symbol using the Symbol() function:

const sym = Symbol();
typeof sym   // "symbol"

// With a description (useful for debugging)
const namedSym = Symbol("mySymbol");
const anotherSym = Symbol("mySymbol");

namedSym === anotherSym   // false - each Symbol is unique

The description string passed to Symbol() is stored internally but has no effect on the symbol’s identity. Two symbols with the same description are still distinct values — the description only matters when you inspect a symbol in the console or convert it to a string. Never rely on the description for comparisons or lookups; the identity lives in the symbol value itself, not in the text you passed to the constructor.

Symbol("test") === Symbol("test")   // false

Since every symbol is unique by design, they offer a natural solution for property key collisions. String keys can clash when different libraries or modules pick the same property name, but a symbol key from one part of the codebase will never equal a symbol key created elsewhere — even if both carry identical description strings.

Using symbols as property keys

Symbols are useful as object property keys because they’re guaranteed to be unique:

const uniqueKey = Symbol("unique");

const obj = {
  [uniqueKey]: "secret value",
  regularKey: "public value"
};

obj[uniqueKey]    // "secret value"
obj.regularKey    // "public value"

This makes Symbols ideal for:

  • Defining private-like properties
  • Adding properties to objects without risking name conflicts
  • Creating constants that won’t clash with other code

Well-Known Symbols

JavaScript provides several built-in “well-known” Symbols that customize object behavior:

Symbol.iterator

Defines the default iterator for an object:

const iterable = {
  [Symbol.iterator]: function* () {
    yield 1;
    yield 2;
    yield 3;
  }
};

[...iterable]   // [1, 2, 3]

Symbol.iterator controls what happens when a for...of loop or spread operator consumes your object. Other well-known symbols govern different aspects of how JavaScript interacts with your objects — from how they appear in string form to how they behave with operators.

Symbol.toStringTag

Customizes the default string description:

const obj = {
  [Symbol.toStringTag]: "MyCustomObject"
};

Object.prototype.toString.call(obj)   // "[object MyCustomObject]"

While Symbol.toStringTag influences the string representation of an object, Symbol.hasInstance changes how instanceof determines whether a value belongs to a given class. This lets you define custom type checks that go beyond prototype chain inspection.

Symbol.hasInstance

Allows customizing instanceof behavior:

class CustomArray {
  static [Symbol.hasInstance](instance) {
    return Array.isArray(instance);
  }
}

[] instanceof CustomArray   // true

Symbol.hasInstance lets you override the class membership check. Symbol.toPrimitive operates at an even lower level — it lets you control what happens when JavaScript coerces your object to a number or a string. This hook fires during arithmetic, string concatenation, and other implicit conversions.

Symbol.toPrimitive

Customizes object-to-primitive conversion:

const obj = {
  value: 10,
  [Symbol.toPrimitive](hint) {
    return this.value;
  }
};

+obj    // 10
obj + 5 // 15

Well-known symbols define how objects behave with language features. Once you have created custom symbols and attached them as keys, you occasionally need to retrieve them back from an object. The reflection APIs Object.getOwnPropertySymbols() and Symbol.for() give you two different paths to do that — one for local symbols and one for globally shared ones.

Getting symbol values

Access existing symbols using Object.getOwnPropertySymbols():

const sym = Symbol("test");
const obj = { [sym]: "value" };

Object.getOwnPropertySymbols(obj)   // [Symbol(test)]

Object.getOwnPropertySymbols() returns every symbol-keyed property on a specific object — a snapshot of locally attached symbols that you created and stored on that instance. For symbols that need to be shared across modules or realms, the global registry accessed through Symbol.for() provides a cross-cutting lookup mechanism that returns the same symbol for a given key every time.

const globalSym = Symbol.for("shared");
const sameSym = Symbol.for("shared");

globalSym === sameSym   // true - same symbol from global registry

Symbol.keyFor(globalSym)   // "shared"

The global registry and reflection APIs cover symbol retrieval. The patterns below show how to apply symbols to two common design problems: using them as enum-style discriminators for switch logic, and using them as semi-private keys for class internals that stay hidden from casual access.

Practical Examples

Creating enum-like values

const Direction = {
  UP: Symbol("UP"),
  DOWN: Symbol("DOWN"),
  LEFT: Symbol("LEFT"),
  RIGHT: Symbol("RIGHT")
};

function move(direction) {
  switch (direction) {
    case Direction.UP:    return "moving up";
    case Direction.DOWN:  return "moving down";
    // ...
  }
}

move(Direction.UP)   // "moving up"

The enum-like pattern works well when you need a fixed set of mutually exclusive options that will not collide with string-based constants from other code. For a different use case — attaching internal state to class instances — you can store a symbol as a module-level constant and use it as a key that outside code cannot guess.

Defining private properties

const _counter = Symbol("counter");

class Counter {
  constructor() {
    this[_counter] = 0;
  }
  
  increment() {
    this[_counter]++;
    return this[_counter];
  }
}

const c = new Counter();
c.increment();   // 1
c._counter;      // undefined - not directly accessible

Limitations

  • Symbols as property keys are not enumerable in for...in loops
  • Object.keys() and Object.getOwnPropertyNames() don’t return Symbol keys
  • Symbols are not serialized in JSON.stringify()
  • Each Symbol() call creates a new unique value

When symbols are a good fit

Symbols are most useful when you need a property key that will not collide with ordinary string-based keys. That makes them handy for library internals, metadata, and protocol-style properties such as Symbol.iterator. They give you a way to attach behavior or hidden state without relying on naming conventions alone.

Because each symbol is unique, you can safely use the same description text in two places without creating a collision. That is useful for debugging, but the description is still only a label. The identity lives in the symbol value itself, not the text inside it.

Global registry and caveats

Symbol.for() is the escape hatch when you do want shared identity across modules. It looks up a symbol by key in the global registry and returns the same symbol each time. That can be useful for conventions that need to cross package boundaries, but it also means the key space is shared, so choose names carefully.

Symbols also have some practical limits. They are not a replacement for privacy, and they do not disappear from all reflection APIs. Use them when you need a unique key with well-defined behavior, not when you need cryptographic secrecy.

See Also