|$ curl https://forge-ai.dev/api/markdown?path=docs/js/symbols
$cat docs/javascript-—-symbols.md
updated This week·20 min read·published

JavaScript — Symbols

JavaScriptSymbolsAdvancedAdvanced
Introduction

Symbols are a primitive data type introduced in ES2015, designed to create unique, immutable identifiers. Unlike strings or numbers, every symbol is guaranteed to be unique — even symbols created with the same description are different values. This uniqueness makes symbols ideal for avoiding property name collisions.

Beyond simple uniqueness, symbols power JavaScript's protocol system through well-known symbols like Symbol.iterator, Symbol.toStringTag, and Symbol.hasInstance. These symbols allow objects to customize their behavior in language-level operations.

This page covers creating symbols, the symbol registry, all well-known symbols, and practical use cases.

symbol-intro.js
JavaScript
1// Creating symbols — each is unique
2const sym1 = Symbol();
3const sym2 = Symbol("debug-name");
4
5console.log(typeof sym1); // "symbol"
6console.log(sym1 === sym2); // false — always unique
7console.log(sym2.description); // "debug-name"
8
9// Symbols as object keys — hidden from normal enumeration
10const obj = {
11 [sym1]: "secret value",
12 visible: "public value"
13};
14
15console.log(obj[sym1]); // "secret value"
16console.log(obj.visible); // "public value"
17console.log(Object.keys(obj)); // ["visible"]
18console.log(Object.getOwnPropertyNames(obj)); // ["visible"]
19console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(debug-name)]
Creating Symbols

The Symbol() function creates a new unique symbol. An optional description string can be provided for debugging — it is accessible via the description property but does not affect uniqueness. There are two ways to create symbols: local symbols via Symbol() and shared symbols via Symbol.for().

MethodDescriptionUniqueness
Symbol()Creates a unique unnamed symbolAlways unique
Symbol("desc")Creates a unique symbol with descriptionAlways unique
Symbol.for("key")Retrieves or creates a global registry symbolShared (same key = same symbol)
Symbol.keyFor(sym)Returns the key for a registry symbol
creating-symbols.js
JavaScript
1// Local symbols — always unique
2const a = Symbol("id");
3const b = Symbol("id");
4
5console.log(a === b); // false — different symbols
6console.log(a.description); // "id"
7console.log(b.description); // "id"
8
9// Global registry symbols — shared across realms
10const shared = Symbol.for("app.api.key");
11const same = Symbol.for("app.api.key");
12
13console.log(shared === same); // true — same symbol
14console.log(Symbol.keyFor(shared)); // "app.api.key"
15
16// Symbols are not auto-converted to strings
17try {
18 console.log("Symbol: " + a);
19} catch (e) {
20 console.log(e.message);
21 // Cannot convert a Symbol value to a string
22}
23
24// Must explicitly call toString() or use description
25console.log(a.toString()); // "Symbol(id)"
26console.log(String(a)); // "Symbol(id)"
27
28// Symbols in object literals
29const user = {
30 name: "Alice",
31 [Symbol("id")]: 12345,
32 [Symbol.for("role")]: "admin"
33};
34
35// Object.getOwnPropertySymbols reveals symbol keys
36const symbols = Object.getOwnPropertySymbols(user);
37console.log(symbols.length); // 2

info

Use Symbol.for() when you need to share a symbol across modules or across different realms (e.g., iframes, workers). Use plain Symbol() when you need a truly private, unique identifier that should not be accessible by external code.
Well-Known Symbols

JavaScript defines built-in symbols that act as protocol hooks. When an object defines a method with a well-known symbol key, JavaScript's runtime uses that method for specific language operations. These are the foundation of protocols like iteration, string coercion, and type checking.

SymbolPurposeUsed By
Symbol.iteratorReturns default iteratorfor..of, spread, yield*
Symbol.asyncIteratorReturns async iteratorfor await..of
Symbol.toStringTagCustom Object.prototype.toStringObject.prototype.toString
Symbol.hasInstanceCustom instanceof behaviorinstanceof operator
Symbol.toPrimitiveCustom type coercionType conversion (String, Number, etc.)
Symbol.speciesConstructor for derived objectsArray.map, Promise.then, etc.
Symbol.matchCustom pattern matchingString.prototype.match
Symbol.replaceCustom string replacementString.prototype.replace
Symbol.searchCustom string searchString.prototype.search
Symbol.splitCustom string splitString.prototype.split
Symbol.isConcatSpreadableControl flattening in concatArray.prototype.concat
Symbol.unscopablesExclude properties from withwith statement

Symbol.iterator — Custom Iteration

symbol-iterator.js
JavaScript
1// Making an object iterable with Symbol.iterator
2const range = {
3 start: 1,
4 end: 5,
5 [Symbol.iterator]() {
6 let current = this.start;
7 const end = this.end;
8 return {
9 next() {
10 if (current <= end) {
11 return { value: current++, done: false };
12 }
13 return { value: undefined, done: true };
14 }
15 };
16 }
17};
18
19// Now works with for..of and spread
20for (const n of range) {
21 console.log(n); // 1, 2, 3, 4, 5
22}
23
24console.log([...range]); // [1, 2, 3, 4, 5]
25
26// Async iterator with Symbol.asyncIterator
27const asyncRange = {
28 start: 1,
29 end: 3,
30 [Symbol.asyncIterator]() {
31 let current = this.start;
32 const end = this.end;
33 return {
34 async next() {
35 await new Promise(r => setTimeout(r, 100));
36 if (current <= end) {
37 return { value: current++, done: false };
38 }
39 return { value: undefined, done: true };
40 }
41 };
42 }
43};
44
45// for await (const n of asyncRange) { console.log(n); }

Symbol.toStringTag & Symbol.hasInstance

well-known-usage.js
JavaScript
1// Custom toString tag
2class MyCollection {
3 get [Symbol.toStringTag]() {
4 return "MyCollection";
5 }
6}
7
8const c = new MyCollection();
9console.log(Object.prototype.toString.call(c));
10// "[object MyCollection]"
11
12// Without Symbol.toStringTag:
13// "[object Object]"
14
15// Custom instanceof behavior
16class Zero {
17 static [Symbol.hasInstance](instance) {
18 return instance === 0 || instance?.valueOf?.() === 0;
19 }
20}
21
22console.log(0 instanceof Zero); // true
23console.log(new Number(0) instanceof Zero); // true
24console.log(1 instanceof Zero); // false
25
26// Symbol.toPrimitive — custom type coercion
27const temperature = {
28 value: 25,
29 unit: "C",
30 [Symbol.toPrimitive](hint) {
31 if (hint === "string") return `${this.value}°${this.unit}`;
32 if (hint === "number") return this.value;
33 return this.value;
34 }
35};
36
37console.log(String(temperature)); // "25°C"
38console.log(+temperature); // 25
39console.log(`Temp: ${temperature}`); // "Temp: 25°C"
preview
🔥

pro tip

The Symbol.species symbol controls which constructor is used for derived objects in methods like Array.map or Promise.then. Overriding it lets you return custom types from built-in methods. For example, a custom array subclass can ensure map() returns an instance of your subclass instead of plain Array.
Symbol Registry

The global symbol registry is a cross-realm symbol pool managed by Symbol.for() and Symbol.keyFor(). Symbols stored here are shared across all realms (iframes, workers, etc.) and can be accessed by their string key. This is distinct from creating symbols with Symbol(), which are always unique and private.

symbol-registry.js
JavaScript
1// Global symbol registry — cross-realm sharing
2// Symbol.for(key) — returns existing or creates new
3const sym1 = Symbol.for("app.config");
4const sym2 = Symbol.for("app.config");
5
6console.log(sym1 === sym2); // true — same symbol
7
8// Symbol.keyFor(sym) — get key from registry symbol
9console.log(Symbol.keyFor(sym1)); // "app.config"
10
11// Non-registry symbols return undefined for keyFor
12const local = Symbol("app.config");
13console.log(Symbol.keyFor(local)); // undefined
14
15// Registry symbols survive across realms
16// In an iframe: Symbol.for("app.config") === sym1 → true
17
18// Practical: API for registering extension points
19const ExtensionPoints = {
20 register(name) {
21 return Symbol.for(`ext.${name}`);
22 },
23 get(name) {
24 const sym = Symbol.for(`ext.${name}`);
25 return Symbol.keyFor(sym) ? sym : undefined;
26 }
27};
28
29const beforeRender = ExtensionPoints.register("beforeRender");
30const afterRender = ExtensionPoints.register("afterRender");
31
32// Now any module can share these symbols
33const plugin = {
34 [beforeRender]: (ctx) => console.log("pre", ctx),
35 [afterRender]: (ctx) => console.log("post", ctx)
36};
37
38console.log(ExtensionPoints.get("beforeRender") === beforeRender);
39// true

warning

Registry symbols are NOT private — any code with access to the key can retrieve the symbol using Symbol.for(). Use registry symbols for shared protocols and extension points. Use plain Symbol() for internal implementation details you want to keep hidden from external consumers.
Use Cases

Private Properties

Before private class fields (#property), symbols were the primary way to create non-enumerable, hard-to-access properties. They are not truly private (accessible via Object.getOwnPropertySymbols), but they avoid accidental name collisions.

private-properties.js
JavaScript
1// Symbols as pseudo-private properties
2const _id = Symbol("id");
3const _cache = Symbol("cache");
4
5class DataStore {
6 constructor() {
7 this[_id] = crypto.randomUUID();
8 this[_cache] = new Map();
9 this.name = "Public";
10 }
11
12 get id() { return this[_id]; }
13
14 setCached(key, value) {
15 this[_cache].set(key, value);
16 }
17
18 getCached(key) {
19 return this[_cache].get(key);
20 }
21}
22
23const store = new DataStore();
24
25// Symbol properties not visible in normal iteration
26console.log(Object.keys(store)); // ["name"]
27console.log(JSON.stringify(store)); // {"name":"Public"}
28
29// But accessible via getOwnPropertySymbols
30const symbols = Object.getOwnPropertySymbols(store);
31console.log(symbols); // [Symbol(id), Symbol(cache)]
32
33// This is why private class fields (#) were introduced
34// They are truly inaccessible from outside the class

Protocols & Extension Points

protocols.js
JavaScript
1// Symbols define protocols — any object can participate
2const Serializable = Symbol("Serializable");
3
4class User {
5 constructor(name, age) {
6 this.name = name;
7 this.age = age;
8 }
9
10 [Serializable]() {
11 return JSON.stringify({
12 name: this.name,
13 age: this.age
14 });
15 }
16}
17
18// Any object can implement the protocol
19const settings = {
20 theme: "dark",
21 lang: "en",
22 [Serializable]() {
23 return JSON.stringify(this);
24 }
25};
26
27function serialize(obj) {
28 if (typeof obj[Serializable] === "function") {
29 return obj[Serializable]();
30 }
31 throw new Error("Not serializable");
32}
33
34console.log(serialize(new User("Alice", 30)));
35console.log(serialize(settings));
36
37// Metadata annotations with symbols
38const Metadata = Symbol("metadata");
39
40function annotate(target, meta) {
41 target[Metadata] = { ...target[Metadata], ...meta };
42 return target;
43}
44
45@annotate({ author: "alice", version: "1.0" })
46class Service {
47 execute() { /* ... */ }
48}
49
50console.log(Service[Metadata]);
51// { author: "alice", version: "1.0" }

best practice

Symbols are ideal for framework and library authors who need to attach metadata to user-provided objects without risk of property name collisions. React uses them for internal fiber fields, and many testing libraries use symbols to attach assertion metadata. For application-level code, prefer Map or WeakMap for storing metadata externally.
Best Practices
Use Symbol() for unique, collision-free property keys — especially for library internals
Use Symbol.for() for shared protocols that need cross-realm access
Prefer private class fields (#) over symbols for truly private state
Always provide a descriptive string to Symbol() for debugging
Use well-known symbols to integrate with JavaScript protocols (iteration, coercion, etc.)
Do not rely on symbols for security — they are discoverable via getOwnPropertySymbols
Use Object.getOwnPropertySymbols to inspect symbol-keyed properties
Avoid symbol registry key collisions by using namespaced keys (e.g., "libname.property")
🔥

pro tip

When serializing objects with symbol keys, JSON.stringify silently drops symbol-keyed properties. If you need to preserve symbol properties during serialization, implement a custom toJSON method or use a replacer function that explicitly handles symbols via Object.getOwnPropertySymbols.
$Blueprint — Engineering Documentation·Section ID: JS-SYMBOLS·Revision: 1.0