JavaScript Symbols: The Fairy Tale of Unique Identity
The Symbol Fairy Story
Once upon a time, in the magical kingdom of JavaScript, there lived many objects who shared their treasures (properties) with anyone who knew their names. But this caused chaos! Sometimes, different wizards (developers) would accidentally use the same property names, causing conflicts and overwriting each other's magical spells.
The JavaScript council decided they needed something special—a way to create truly unique keys that could never be duplicated, even if they had the same description. So they summoned the Symbol Fairy, who had the power to create mystical keys that were guaranteed to be unique across the entire kingdom.

Symbol is some primitive data type nothing very different from other primitive data types (number, string, boolean etc..) supported by JavaScript. So you want a symbol? Just remember the following:
1 - Symbol fairy (an inbuilt JavaScript object) distributes unique symbols
2 - She has a wand (the constructor of the Symbol object)
3 - To make a wish you just say Symbol() or you can add your own description to the wish Symbol("some_key"), that doesn't make much difference. No matter how many times you make the same wish you will get a different symbol.
4 - Very important, you cannot ask for a new wand itself. No cheating! new Symbol() - Throws a typeError in JavaScript. So, its only Symbol() and not new Symbol().
5 - Symbol fairy has a secret, if you ask for your wish like this - Symbol.for("some_key"), she stores all the symbols you asked for, in the fairyland (Global Symbol Registry) and you can get the same Symbol again by calling Symbol.for("some_key") anytime. Don't worry this does not break the rule of duplicates. You just keep getting the same symbol that's stored for you in the Global Symbol Registry for some purpose.
Alright let's come back to life and discuss Symbols quickly. We now know that Symbols are nothing but unique values returned when we make a call to the constructor of the inbuilt Symbol object. Symbol('myKey') != Symbol('yourKey') and unsurprisingly Symbol('myKey') != Symbol('myKey'). Every Symbol() call is guaranteed to return a unique Symbol. We also know that by simply saying Symbol.for('myKey') we create a Symbol in the Global Registry and if it was already created earlier, you get access to the same old Symbol. Nothing complicated!
What Are Symbols?
Symbols are a primitive data type introduced in ES6 (ES2015) that represents a unique identifier. Unlike strings or numbers, every symbol is guaranteed to be unique, even if created with the same description.
const sym1 = Symbol('id');
const sym2 = Symbol('id'); // optional description 'id'
const sym3 = Symbol();
console.log(sym1 === sym2); // false - they're unique!
Symbols are immutable and cannot be changed once created. They serve as unique property keys for objects, helping prevent accidental property name collisions.
Properties of Symbols
Here are some key properties of Symbols:
1. Unique: Each Symbol is unique and cannot be duplicated.
const a = Symbol('test');
const b = Symbol('test');
console.log(a === b); // false - always unique
2. Immutable: Symbols cannot be changed or modified once created.
const sym = Symbol('immutable');
// You cannot modify a symbol once created
3. Not convertible: Symbols cannot be converted to a different type, such as a string or number.
const sym = Symbol('test');
console.log(sym + ''); // TypeError: Cannot convert a Symbol value to a string
console.log(Number(sym)); // TypeError: Cannot convert a Symbol value to a number
4. Non-enumerable by nature: Symbols are hidden from normal object enumeration.
const sym = Symbol('hidden');
const obj = {
visible: 'shown',
[sym]: 'hidden'
};
// Symbol properties don't show up in normal enumeration
console.log(Object.keys(obj)); // ['visible'] - symbol not included
for (let key in obj) {
console.log(key); // Only logs 'visible'
}
Why Creating new Symbol() Throws an Error
Symbol is a primitive data type, not an object constructor. Unlike other constructors (like new Object() or new Array()), Symbol is designed to return primitive values, not object instances.
// This throws TypeError - you cannot steal Symbol fairy's wand!
// const sym = new Symbol('test'); // TypeError: Symbol is not a constructor
// Correct way - make a wish to Symbol fairy
const sym = Symbol('test'); // ✓ Correct
Why this design choice?
- Symbols are meant to be lightweight primitive values, not objects
- Prevents confusion about whether symbols are objects or primitives
- Ensures consistent behavior - symbols are always primitives
- Avoids the overhead of object creation for what should be simple unique identifiers
Do We Really Need Symbols?
Use symbols when your requirement is one of these:
1. Enums
Create constants that are guaranteed to be unique:
const Colors = {
RED: Symbol('red'),
GREEN: Symbol('green'),
BLUE: Symbol('blue')
};
// These will never conflict with any other values
function processColor(color) {
switch (color) {
case Colors.RED:
return 'Stop!';
case Colors.GREEN:
return 'Go!';
default:
return 'Unknown';
}
}
2. Name Clashes
Prevent property conflicts in objects:
// Library A
const LibA_CONFIG = Symbol('config');
// Library B
const LibB_CONFIG = Symbol('config');
const app = {};
app[LibA_CONFIG] = { theme: 'dark' };
app[LibB_CONFIG] = { debug: true };
// No conflicts! Both configs coexist
3. Privacy
Create pseudo-private properties:
const _balance = Symbol('balance');
const _id = Symbol('id');
class BankAccount {
constructor(id, balance) {
this[_id] = id;
this[_balance] = balance;
}
getBalance() {
return this[_balance];
}
}
const account = new BankAccount('123', 1000);
console.log(account.getBalance()); // 1000
// account._balance is undefined - cannot access directly
4. Well-known Symbols
Customize object behavior:
const myIterable = {
data: [1, 2, 3],
[Symbol.iterator]: function* () {
for (let item of this.data) {
yield item;
}
}
};
for (let value of myIterable) {
console.log(value); // 1, 2, 3
}
Alternative approaches without symbols:
- Use conventions like
_private(not truly private) - Use WeakMaps for privacy (more complex)
- Use string keys with unique prefixes (still can collide)
Why Were Symbols Needed?
Before symbols, JavaScript developers faced several challenges:
1. Property Name Collisions
When working with objects, especially in large applications or when integrating third-party libraries, property names could accidentally clash:
// Without symbols - potential conflicts
const user = { name: 'Alice' };
// Library A adds
user.id = 'lib-a-id';
// Library B overwrites it!
user.id = 'lib-b-id';
2. No True Privacy
JavaScript didn't have a built-in way to create truly private properties:
// Properties are always visible
const obj = { secret: 'hidden' };
console.log(obj.secret); // Anyone can access it
3. Need for Meta-Programming
JavaScript needed a way to add special behaviors to objects without interfering with normal properties.
Creating and Using Symbols
Basic Symbol Creation
// Create a symbol - Symbol fairy grants unique symbols
const mySymbol = Symbol();
const namedSymbol = Symbol('description');
// IMPORTANT: Cannot use 'new' keyword
// new Symbol(); // TypeError: Symbol is not a constructor
// The description is just for debugging
console.log(mySymbol.toString()); // "Symbol()"
console.log(namedSymbol.toString()); // "Symbol(description)"
Using Symbols as Object Properties
const id = Symbol('id');
const name = Symbol('name');
const user = {
[id]: 12345,
[name]: 'Alice',
email: 'alice@example.com'
};
console.log(user[id]); // 12345
console.log(user[name]); // Alice
console.log(user.email); // alice@example.com
Symbol Properties Are Hidden from Enumeration
const secret = Symbol('secret'); // first create Symbol, add as the property to the object
const obj = {
visible: 'everyone can see this',
[secret]: 'hidden treasure'
};
// Symbol is non-enumerable by nature
// Regular enumeration doesn't show symbols
console.log(Object.keys(obj)); // ['visible']
for (let key in obj) {
console.log(key); // Only logs 'visible'
}
// Object.getOwnPropertyNames() also doesn't include symbols
console.log(Object.getOwnPropertyNames(obj)); // ['visible']
// But you can still access it if you know the symbol
console.log(obj[secret]); // 'hidden treasure'
Global Symbol Registry (The Fairyland)
JavaScript provides a global symbol registry (the fairyland) for symbols that need to be shared across different parts of your application:
// Create or retrieve a symbol from global registry (fairyland)
const globalSym1 = Symbol.for('app.id');
const globalSym2 = Symbol.for('app.id');
console.log(globalSym1 === globalSym2); // true - same symbol from fairyland!
// Get the key for a global symbol
console.log(Symbol.keyFor(globalSym1)); // 'app.id'
// Regular symbols vs Global symbols
const regular1 = Symbol('test');
const regular2 = Symbol('test');
console.log(regular1 === regular2); // false - always unique
const global1 = Symbol.for('test');
const global2 = Symbol.for('test');
console.log(global1 === global2); // true - same from fairyland
When to use global symbols:
- Cross-module communication
- Plugin systems
- Shared configuration keys
Well-Known Symbols
JavaScript provides several built-in symbols that control object behavior:
Symbol.iterator
Makes objects iterable:
const myIterable = {
data: ['a', 'b', 'c'],
[Symbol.iterator]: function* () {
for (let item of this.data) {
yield item;
}
}
};
for (let value of myIterable) {
console.log(value); // 'a', 'b', 'c'
}
Symbol.toStringTag
Customizes the Object.prototype.toString() result:
class MyClass {
get [Symbol.toStringTag]() {
return 'MyCustomClass';
}
}
const instance = new MyClass();
console.log(instance.toString()); // "[object MyCustomClass]"
Symbol.hasInstance
Customizes instanceof behavior:
class MyArray {
static [Symbol.hasInstance](instance) {
return Array.isArray(instance);
}
}
console.log([] instanceof MyArray); // true
Other Well-Known Symbols
Symbol.asyncIterator- Async iterationSymbol.match- String matching behaviorSymbol.replace- String replacement behaviorSymbol.search- String search behaviorSymbol.split- String splitting behaviorSymbol.toPrimitive- Type conversionSymbol.species- Constructor for derived objects
Practical Use Cases
1. Creating Private Properties
const _id = Symbol('id');
const _balance = Symbol('balance');
class BankAccount {
constructor(id, initialBalance) {
this[_id] = id;
this[_balance] = initialBalance;
}
getBalance() {
return this[_balance];
}
deposit(amount) {
this[_balance] += amount;
}
}
const account = new BankAccount('123', 1000);
console.log(account.getBalance()); // 1000
console.log(account[_balance]); // undefined (symbol not accessible)
2. Avoiding Library Conflicts
// Library A - gets unique symbol from Symbol fairy
const LibA_ID = Symbol('LibA.id');
function attachLibAData(obj, id) {
obj[LibA_ID] = id;
}
// Library B - gets different unique symbol
const LibB_ID = Symbol('LibB.id');
function attachLibBData(obj, id) {
obj[LibB_ID] = id;
}
// Usage - no conflicts because Symbol fairy ensures uniqueness!
const user = {};
attachLibAData(user, 'a-123');
attachLibBData(user, 'b-456');
// Both IDs coexist peacefully
3. Meta-Programming
const validators = Symbol('validators');
class User {
constructor(name, email) {
this.name = name;
this.email = email;
this[validators] = [];
}
addValidator(fn) {
this[validators].push(fn);
}
validate() {
return this[validators].every(validator => validator(this));
}
}
4. Enums with Symbols
const Colors = {
RED: Symbol('red'),
GREEN: Symbol('green'),
BLUE: Symbol('blue')
};
function processColor(color) {
switch (color) {
case Colors.RED:
return 'Stop!';
case Colors.GREEN:
return 'Go!';
case Colors.BLUE:
return 'Cool!';
default:
return 'Unknown color';
}
}
Symbol vs String Keys
| Aspect | Symbols | Strings |
|---|---|---|
| Uniqueness | Always unique (Symbol fairy magic) | Can collide |
| Enumeration | Hidden from for...in, Object.keys() | Visible in enumeration |
| JSON.stringify | Ignored | Included |
| Property access | obj[symbol] only | obj.key or obj['key'] |
| Debugging | Has optional description | Self-descriptive |
| Creation | Symbol() or Symbol.for() | 'string' or new String() |
| Type conversion | Cannot convert to string/number | Can convert |
Getting Symbol Properties
While symbols are hidden from normal enumeration, you can still access them if needed:
const sym1 = Symbol('prop1');
const sym2 = Symbol('prop2');
const obj = {
normal: 'visible',
[sym1]: 'hidden1',
[sym2]: 'hidden2'
};
// Get symbol properties specifically
console.log(Object.getOwnPropertySymbols(obj)); // [sym1, sym2]
// Get all property descriptors (including symbols)
console.log(Reflect.ownKeys(obj)); // ['normal', sym1, sym2]
// Regular methods don't show symbols
console.log(Object.keys(obj)); // ['normal']
console.log(Object.getOwnPropertyNames(obj)); // ['normal']
Best Practices
1. Use Descriptive Names
// Good - describe your wish clearly to Symbol fairy
const userId = Symbol('user.id');
const isAdmin = Symbol('user.isAdmin');
// Not so good
const a = Symbol();
const b = Symbol('x');
2. Keep Symbol References
// Store symbols for later use - Symbol fairy doesn't repeat wishes
const SYMBOLS = {
ID: Symbol('id'),
ROLE: Symbol('role'),
PERMISSIONS: Symbol('permissions')
};
// Use throughout your application
user[SYMBOLS.ID] = 12345;
3. Use Global Registry (Fairyland) Sparingly
// Only for truly global, shared symbols - fairyland access
const GLOBAL_EVENT_BUS = Symbol.for('app.eventBus');
// Prefer local symbols for most use cases
const localSecret = Symbol('secret');
4. Document Symbol Usage
/**
* @private
* @symbol _internalState - Stores internal component state
*/
const _internalState = Symbol('internalState');
Common Pitfalls
1. Symbols Are Not Strings
const sym = Symbol('test');
console.log(typeof sym); // 'symbol', not 'string'
console.log(sym == 'test'); // false
console.log(sym + ''); // TypeError: Cannot convert a Symbol value to a string
2. Symbol Descriptions Are Just Hints
const sym1 = Symbol('same');
const sym2 = Symbol('same');
console.log(sym1 === sym2); // false - Symbol fairy gives unique symbols despite same description
3. JSON Serialization Ignores Symbols
const obj = {
normal: 'visible',
[Symbol('hidden')]: 'invisible'
};
console.log(JSON.stringify(obj)); // {"normal":"visible"}
Why does JSON.stringify ignore symbols?
- JSON format doesn't support symbols as they are a JavaScript-specific primitive type
- Symbols are meant for internal object mechanics, not for data exchange
- Including symbols in JSON would break interoperability with other languages
Converting Symbols to Strings
If you need to include symbol-keyed properties in JSON, you have a few options:
Option 1: Manual conversion with custom logic
const sym = Symbol('id');
const obj = {
name: 'Alice',
[sym]: 12345
};
// Convert symbol properties manually
function serializeWithSymbols(obj) {
const result = { ...obj }; // copy regular properties
// Get symbol properties and convert them
Object.getOwnPropertySymbols(obj).forEach(symbol => {
// Use symbol description as string key
const key = symbol.description || symbol.toString();
result[key] = obj[symbol];
});
return JSON.stringify(result);
}
console.log(serializeWithSymbols(obj)); // {"name":"Alice","id":12345}
Option 2: Custom replacer function
const sym = Symbol('secret');
const obj = {
visible: 'data',
[sym]: 'hidden data'
};
function symbolReplacer(key, value) {
if (typeof key === 'symbol') {
return `Symbol(${key.description})`;
}
return value;
}
// This still won't include symbol properties, but shows the concept
console.log(JSON.stringify(obj, symbolReplacer));
Option 3: Use a transformation function
const sym1 = Symbol('id');
const sym2 = Symbol('type');
const obj = {
name: 'Test',
[sym1]: 123,
[sym2]: 'user'
};
function transformSymbolsToStrings(obj) {
const transformed = {};
// Copy regular properties
Object.keys(obj).forEach(key => {
transformed[key] = obj[key];
});
// Convert symbol properties to string keys
Object.getOwnPropertySymbols(obj).forEach(symbol => {
const stringKey = `__symbol_${symbol.description || 'unnamed'}`;
transformed[stringKey] = obj[symbol];
});
return transformed;
}
const serializable = transformSymbolsToStrings(obj);
console.log(JSON.stringify(serializable));
// {"name":"Test","__symbol_id":123,"__symbol_type":"user"}
4. Cannot Use 'new' Keyword
// This throws TypeError - you cannot steal Symbol fairy's wand!
// const sym = new Symbol('test'); // TypeError: Symbol is not a constructor
// Correct way - make a wish to Symbol fairy
const sym = Symbol('test'); // ✓ Correct
5. Symbols Are Non-Enumerable
const sym = Symbol('hidden');
const obj = { visible: 'shown', [sym]: 'hidden' };
// These won't show the symbol property
for (let key in obj) { /* only 'visible' */ }
Object.keys(obj); // ['visible']
Object.getOwnPropertyNames(obj); // ['visible']
// Use these to find symbols
Object.getOwnPropertySymbols(obj); // [sym]
Reflect.ownKeys(obj); // ['visible', sym]
Advanced Symbol Patterns
Creating a Symbol-Based Event System
const EventSymbols = {
LISTENERS: Symbol('listeners'),
EMIT: Symbol('emit'),
ON: Symbol('on'),
OFF: Symbol('off')
};
function makeEventEmitter(obj) {
obj[EventSymbols.LISTENERS] = new Map();
obj[EventSymbols.ON] = function(event, listener) {
if (!this[EventSymbols.LISTENERS].has(event)) {
this[EventSymbols.LISTENERS].set(event, []);
}
this[EventSymbols.LISTENERS].get(event).push(listener);
};
obj[EventSymbols.EMIT] = function(event, ...args) {
const listeners = this[EventSymbols.LISTENERS].get(event) || [];
listeners.forEach(listener => listener(...args));
};
return obj;
}
// Usage
const emitter = makeEventEmitter({});
emitter[EventSymbols.ON]('test', data => console.log('Received:', data));
emitter[EventSymbols.EMIT]('test', 'Hello World!');
Summary
Symbols are JavaScript's answer to creating truly unique identifiers that:
- Prevent property collisions in objects (Symbol fairy ensures uniqueness)
- Enable meta-programming through well-known symbols
- Provide pseudo-privacy for object properties
- Support better library design by avoiding naming conflicts
- Are non-enumerable by nature - hidden from normal iteration
Do we really need symbols? Use symbols when your requirement is one of these:
- Enums - Unique constants
- Name Clashes - Avoiding property conflicts
- Privacy - Pseudo-private properties
- Well-known Symbols - Customizing object behavior
Remember the Symbol Fairy Rules:
- Symbol fairy distributes unique symbols - Symbol() always returns unique values
- Use her wand correctly - Symbol(), not new Symbol()
- Global registry (fairyland) - Symbol.for() for shared symbols
- Descriptions are just hints - Symbol('same') !== Symbol('same')
- Every symbol is unique - except those stored in the fairyland
- Symbols are non-enumerable - hidden from for...in, Object.keys()
- Symbols cannot be converted - to strings or numbers
- JSON ignores symbols - use manual conversion if needed for serialization
Remember: Every symbol is unique, descriptions are optional hints, symbols are non-enumerable by nature, and symbols are your friends for creating better, more maintainable JavaScript applications.