Symbols and well-known symbols

Create property keys that can never clash, and use well-known symbols to customise how the language treats your objects.

  • Beginner
  • 7 min read
  • Updated 17 Oct 2026

What you will learn

  • Create symbol-keyed properties
  • Implement a well-known symbol
  • Choose between local and registered symbols

Before you start

A symbol is a primitive value that is guaranteed to be unique. Its main job is to be a property key that cannot collide with any other key, including keys added by other code or by future versions of the language. The language itself uses a set of well-known symbols, such as Symbol.iterator, as hooks that let your objects take part in built-in behaviour.

Creating symbols

Call Symbol(), optionally with a description. There is no new; symbols are primitives, like numbers and strings.

JavaScript
const id = Symbol('id');
const otherId = Symbol('id');

console.log(typeof id);         // → symbol
console.log(id === otherId);    // → false
console.log(id.description);    // → id
console.log(id.toString());     // → Symbol(id)

The description is only a label for debugging. Two symbols with the same description are still different values.

Symbols do not convert to strings automatically, which protects you from accidentally using one as text:

JavaScript
try {
  console.log(`Key: ${id}`);
} catch (error) {
  console.log(error instanceof TypeError); // → true
}

console.log(`Key: ${String(id)}`); // → Key: Symbol(id)

Symbols as property keys

Use a symbol as a key with square brackets, both in object literals and when accessing the property:

JavaScript
const internalId = Symbol('internalId');

const course = {
  title: 'JavaScript',
  [internalId]: 'c-42',
};

console.log(course[internalId]); // → c-42
console.log(course.internalId);  // → undefined (a string key, not the symbol)

Symbol-keyed properties are skipped by most of the ways code looks at an object:

JavaScript
console.log(Object.keys(course));      // → [ 'title' ]
console.log(JSON.stringify(course));   // → {"title":"JavaScript"}
for (const key in course) {
  console.log(key);                    // → title
}

console.log(Object.getOwnPropertySymbols(course)); // → [ Symbol(internalId) ]
console.log(Reflect.ownKeys(course));              // → [ 'title', Symbol(internalId) ]

This makes symbols good for metadata that should not get in the way: a library can attach information to objects it does not own without it showing up in loops, JSON output or the owner's own keys. Object spread and Object.assign do copy enumerable symbol properties, so the metadata travels with shallow copies.

Symbols are not a privacy mechanism. Anyone can find symbol keys with Object.getOwnPropertySymbols or Reflect.ownKeys. For truly private data in a class, use private fields (#name).

The global symbol registry

Sometimes two separate pieces of code, such as two bundles on one page or code in an iframe, need to agree on the same symbol. Symbol.for(key) looks the key up in a registry shared across the whole JavaScript environment, and creates the symbol only if it does not exist yet:

JavaScript
const a = Symbol.for('geeks.cache');
const b = Symbol.for('geeks.cache');

console.log(a === b);              // → true
console.log(Symbol.keyFor(a));     // → geeks.cache
console.log(Symbol.keyFor(Symbol('local'))); // → undefined

Registered symbols trade uniqueness for shareability: any code using the same string gets the same symbol, so namespace your keys ('geeks.cache', not 'cache'). Use plain Symbol() unless you specifically need sharing.

Well-known symbols

The language defines a set of symbols as static properties of Symbol. Built-in operations look for methods under these keys, so implementing one changes how the language treats your object. The most useful:

SymbolUsed byLets you
Symbol.iteratorfor...of, spread, destructuringmake an object iterable
Symbol.asyncIteratorfor await...ofmake an object async iterable
Symbol.toPrimitive+obj, template literals, obj + 1, comparisonscontrol conversion to a primitive
Symbol.toStringTagObject.prototype.toStringname the object in [object Name]
Symbol.hasInstanceinstanceofcustomise instanceof checks

There are others, such as Symbol.species and the regular expression symbols (Symbol.match, Symbol.replace and so on), which you will rarely need.

Symbol.iterator

You met this in the iterators lesson. A collection class that exposes iteration without exposing its internals:

JavaScript
class CourseList {
  #courses = [];

  add(course) {
    this.#courses.push(course);
    return this;
  }

  *[Symbol.iterator]() {
    for (const course of this.#courses) {
      yield course.title;
    }
  }
}

const list = new CourseList().add({ title: 'HTML' }).add({ title: 'CSS' });
console.log([...list]); // → [ 'HTML', 'CSS' ]

Symbol.toPrimitive

When JavaScript needs a primitive from an object, for maths, string interpolation or comparison, it calls [Symbol.toPrimitive](hint) if it exists. The hint is 'number', 'string' or 'default':

JavaScript
class Money {
  constructor(amount, currency) {
    this.amount = amount;
    this.currency = currency;
  }

  [Symbol.toPrimitive](hint) {
    if (hint === 'string') {
      return `${this.amount.toFixed(2)} ${this.currency}`;
    }
    return this.amount;
  }
}

const price = new Money(9.5, 'GBP');

console.log(`${price}`);   // → 9.50 GBP   (hint: 'string')
console.log(+price);       // → 9.5        (hint: 'number')
console.log(price + 1);    // → 10.5       (hint: 'default')
console.log(price > 5);    // → true       (hint: 'number')

Without this method, JavaScript falls back to valueOf() and toString(), and putting price in a template literal would print [object Object]. Use this sparingly: implicit conversion can surprise readers, and an explicit price.format() method is often clearer. It is most useful for value-like objects such as money, durations or measurements.

Symbol.toStringTag

Object.prototype.toString reports an object's type as [object Name]. A getter under Symbol.toStringTag sets the name, which makes logs and type checks clearer:

JavaScript
class Queue {
  get [Symbol.toStringTag]() {
    return 'Queue';
  }
}

console.log(String(new Queue()));                        // → [object Queue]
console.log(Object.prototype.toString.call(new Queue())); // → [object Queue]

Built-ins use the same hook: Object.prototype.toString.call(new Map()) gives [object Map].

Symbol.hasInstance

A static method under Symbol.hasInstance decides what instanceof returns. It turns instanceof into a general type check:

JavaScript
class EvenNumber {
  static [Symbol.hasInstance](value) {
    return Number.isInteger(value) && value % 2 === 0;
  }
}

console.log(4 instanceof EvenNumber);   // → true
console.log(7 instanceof EvenNumber);   // → false
console.log('4' instanceof EvenNumber); // → false

This is clever, but instanceof normally means "was created by this class", and redefining it can confuse readers. A plain function such as isEven(value) is usually the better choice; know the hook exists so you recognise it in libraries.

Practical uses

  • Collision-free extension points. A library can define const serialise = Symbol('serialise') and call obj[serialise]() if present. Users implement the method under that symbol, and it can never clash with an existing serialise property.
  • Metadata on objects you do not own. Attach a symbol-keyed property to mark objects you have processed, without affecting their JSON or keys. Alternatively, use a WeakMap keyed by the object, which leaves the object untouched altogether.
  • Unique constants. Symbols make good enum-like values that cannot be confused with strings: const Status = Object.freeze({ Loading: Symbol('loading'), Ready: Symbol('ready') }). They cannot be serialised to JSON, so use strings for values that are saved or sent over the network.

Since ES2023, non-registered symbols can also be used as WeakMap keys. Registered symbols from Symbol.for cannot, because they never become unreachable.

Summary

  • Symbol(description) creates a unique primitive; the description is only a label.
  • Symbol-keyed properties are skipped by Object.keys, for...in and JSON.stringify, but are not private.
  • Symbol.for(key) returns a shared, registered symbol; namespace the key.
  • Well-known symbols such as Symbol.iterator, Symbol.toPrimitive, Symbol.toStringTag and Symbol.hasInstance let objects plug into built-in behaviour.
  • Use symbols for collision-free keys and extension points, and prefer explicit methods when implicit behaviour would surprise readers.

Further reading