30% offEnding soon

JavaScript Symbol: Interview Guide and Edge Cases

26 min read

A JavaScript symbol is a primitive value with its own identity. Frontend interviews use symbols to test property keys, object enumeration, copying, serialization, coercion, and language protocols such as iteration.

JavaScript Symbol creates identity-based primitive values that work as collision-resistant property keys and as hooks for built-in language behavior, but symbols are discoverable and do not provide private state.

What Is a JavaScript Symbol?

Symbol() returns a symbol primitive. The optional argument is a description that helps people recognize the symbol during debugging. It does not determine the symbol's identity.

Two calls with the same description still return different values:

const first = Symbol("status");
const second = Symbol("status");

console.log(typeof first);
console.log(first === second);
console.log(first.description);
console.log(second.description);
symbol
false
status
status

The descriptions match, but the identities do not. This distinction is the basis of most Symbol interview questions.

Description is not identitySymbol('status')called twiceidentity Astatusidentity Bstatussame display label, different property keys
Matching descriptions do not make matching symbol identities.

A symbol can be an object property key. Use computed property syntax, with square brackets, when defining or reading that property:

const scoreKey = Symbol("score");

const candidate = {
  name: "Mina",
  [scoreKey]: 8,
};

console.log(candidate.name);
console.log(candidate[scoreKey]);
console.log(candidate.scoreKey);
Mina
8
undefined

candidate[scoreKey] uses the symbol stored in the variable. candidate.scoreKey looks for the string key "scoreKey", so it is a different property.

One object, two kinds of keycandidate[scoreKey]candidate.scoreKeycandidate objectsymbol scoreKeyvalue: 8string scoreKeyabsent → undefined
Bracket and dot syntax can address different property keys.

An object property key can be a string or a symbol. A symbol avoids accidental collision because unrelated code cannot recreate a local symbol from its description. The other code needs the original symbol value.

Symbol is callable, but it is not a constructor. Calling it with new throws:

try {
  new Symbol("score");
} catch (error) {
  console.log(error.name);
}
TypeError

This is an identity primitive, not a wrapper object that application code should instantiate.

The distinction between a description and an identity also explains why logging can mislead. Two symbols may both display as Symbol(status) while remaining unequal. Compare the values themselves rather than their printed descriptions.

Three Kinds of Symbols You Must Distinguish

A useful interview model separates symbols into three categories.

  • A local symbol comes from Symbol(). Every call creates a distinct value.
  • A registered symbol comes from Symbol.for(key). Repeated lookups with the same string registry key return the same identity.
  • A well-known symbol is a fixed symbol exposed as a property of Symbol, such as Symbol.iterator. JavaScript operations look for these symbols to customize specific behavior.

A local description and a registry key are different concepts. Symbol("cache") accepts "cache" as a description. It does not register that symbol.

Symbol.for("cache") treats "cache" as a registry key:

const localA = Symbol("cache");
const localB = Symbol("cache");
const registeredA = Symbol.for("cache");
const registeredB = Symbol.for("cache");

console.log(localA === localB);
console.log(registeredA === registeredB);
console.log(localA === registeredA);
false
true
false

A realm is a JavaScript global environment, such as a page or a same-runtime iframe. Realms in the same JavaScript agent share the global symbol registry, so they can agree on the same symbol through a known string key. It also means Symbol.for() does not provide the isolation of a locally created symbol.

Symbol.keyFor() performs the reverse lookup for a registered symbol. It returns that symbol's registry key. A local symbol has no registry key:

const local = Symbol("session");
const registered = Symbol.for("session");

console.log(Symbol.keyFor(local));
console.log(Symbol.keyFor(registered));
undefined
session

Do not derive program logic from description when identity matters. Descriptions can match across unrelated symbols. A registry key has meaning only in the registry, while identity is the actual primitive value used for equality and property access.

Well-known symbols belong to neither of the first two creation patterns in application code. You do not create Symbol.iterator by calling Symbol("iterator"), and Symbol.for("iterator") does not retrieve it. You use the fixed value supplied by JavaScript:

console.log(Symbol("iterator") === Symbol.iterator);
console.log(Symbol.for("iterator") === Symbol.iterator);
console.log(Symbol.iterator === Symbol.iterator);
false
false
true

This three-part model prevents a common interview mistake: saying that every symbol is unique without qualification. Separate Symbol() calls are distinct. Repeated Symbol.for() calls with the same registry key intentionally return a shared identity.

Three identity mechanismsLOCALSymbol('x') × 2new identity Anew identity BREGISTEREDSymbol.for('x')× 2registry key xone shared identityWELL-KNOWNSymbol.iteratorJavaScript suppliesone fixed identitydescription, registry key, and protocol name are notinterchangeable
Local, registered, and well-known symbols obtain identity differently.

How Symbol Properties Behave on Objects

Symbol properties are easy to miss because common enumeration methods focus on string keys. They are still real properties, and reflection methods can find them.

The following object contains an enumerable string property, a non-enumerable string property, and an enumerable symbol property:

const meta = Symbol("meta");
const record = { id: 7, [meta]: "reviewed" };

Object.defineProperty(record, "hidden", {
  value: true,
  enumerable: false,
});

console.log(record[meta]);
console.log(meta in record);
console.log(Object.keys(record));
console.log(Object.getOwnPropertyNames(record));
console.log(Object.getOwnPropertySymbols(record).map(String));
console.log(Reflect.ownKeys(record).map(String));

const loopKeys = [];
for (const key in record) {
  loopKeys.push(key);
}
console.log(loopKeys);

console.log(Object.getOwnPropertySymbols(Object.assign({}, record)).length);
console.log(Object.getOwnPropertySymbols({ ...record }).length);
console.log(JSON.stringify(record));
reviewed
true
[ 'id' ]
[ 'id', 'hidden' ]
[ 'Symbol(meta)' ]
[ 'id', 'hidden', 'Symbol(meta)' ]
[ 'id' ]
1
1
{"id":7}

The matrix makes the rules easier to compare:

Four windows onto the same objectrecordstring: idenumerablestring: hiddennot enumerablesymbol: metaenumerableObject.keys()sees: idOwn string namessees: id + hiddenOwn symbol keyssees: metaReflect.ownKeys()sees all threeskipped by one window does not mean absent
Enumeration methods reveal different slices of one object.
OperationOwn enumerable stringOwn non-enumerable stringInherited enumerable stringInherited non-enumerable stringOwn enumerable symbolOwn non-enumerable symbolInherited symbol
Direct access with the exact keyYesYesYesYesYesYesYes
key in objectYesYesYesYesYesYesYes
Object.keys()YesNoNoNoNoNoNo
Object.getOwnPropertyNames()YesYesNoNoNoNoNo
Object.getOwnPropertySymbols()NoNoNoNoYesYesNo
Reflect.ownKeys()YesYesNoNoYesYesNo
for...inYesNoYesNoNoNoNo
Object.assign()YesNoNoNoYesNoNo
Object spread { ...object }YesNoNoNoYesNoNo
JSON.stringify()YesNoNoNoNoNoNo

Object.getOwnPropertySymbols() returns an array of an object's own symbol keys. Reflect.ownKeys() returns all own keys, including enumerable and non-enumerable string and symbol keys.

Both Object.assign() and object spread copy enumerable own symbol properties. A non-enumerable own symbol remains visible to the reflection methods, but copying operations skip it.

Object spread and iterable spread are separate operations. { ...source } copies enumerable own properties, including symbol properties. [...source] and fn(...source) consume the value through Symbol.iterator.

The punctuation chooses the mechanismsource objectstring propertysymbol property{ ...source }new objectstring copiedsymbol copiediterable objectSymbol.iteratornext() → values[...source]new arrayproduced valuesbraces inspect properties; brackets run the iterableprotocol
Object spread copies properties; iterable spread pulls produced values.

Symbol keys and symbol values follow different rules

A symbol-keyed property uses the symbol on the left side of the property relationship. A symbol value is an ordinary value stored under some key.

First ask: where is the symbol?symbol key→ ordinary valuemetadataproperty omittedfrom JSONstring keysymbol valuesavedValueproperty omittedfrom JSONarray positioncontains asymbol valueposition remainsas nullobject properties can disappear; array positionscannot collapse
JSON treats symbol keys, object values, and array elements differently.

This object contains both:

const internalKey = Symbol("internal");
const token = Symbol("token");

const data = {
  visibleToken: token,
  [internalKey]: "metadata",
  items: [token],
};

console.log(Object.keys(data));
console.log(Object.getOwnPropertySymbols(data).map(String));
console.log(JSON.stringify(data));
[ 'visibleToken', 'items' ]
[ 'Symbol(internal)' ]
{"items":[null]}

visibleToken is a string key, even though its value is a symbol. Object.keys() includes that key. During JSON serialization, its symbol value causes the property to be omitted.

The internalKey property is omitted because JSON.stringify() ignores symbol-keyed properties. The symbol inside the array becomes null, preserving the array position.

This distinction often appears beside questions about JSON.stringify(). First identify where the symbol appears. A symbol used as a key and a symbol used as a value do not take the same path through every operation.

Practical Uses and Where Symbols Fall Short

A local symbol works well when one piece of code needs to attach metadata without competing with ordinary string keys.

Suppose a validation helper records which fields it checked. The application object may already contain a string property named validatedFields, so a symbol keeps the helper's metadata in a separate property slot:

const validatedFields = Symbol("validatedFields");

function validateProfile(profile) {
  const checked = ["name", "email"];

  return {
    ...profile,
    [validatedFields]: checked,
  };
}

const profile = validateProfile({
  name: "Ari",
  email: "[email protected]",
  validatedFields: "user supplied value",
});

console.log(profile.validatedFields);
console.log(profile[validatedFields].join(", "));
console.log(Reflect.ownKeys(profile).map(String));
user supplied value
name, email
[ 'name', 'email', 'validatedFields', 'Symbol(validatedFields)' ]

The string property and symbol property coexist. Code with the validatedFields symbol can access the metadata. Code that happens to choose the same string cannot overwrite it.

This provides collision avoidance, not privacy. Any code holding the object can inspect its symbol keys and read their values. It can modify ordinary writable symbol properties, but symbols themselves provide no access control.

Separate slot, visible doorprofile objectstring keyvalidatedFieldssymbol keyvalidatedFieldsuser codesame namereflectionsees bothcollision avoidedprivacy not created
Symbols prevent name collisions but do not hide data.

Choose the storage mechanism according to the actual requirement:

RequirementSuitable choiceMain reason
A normal public field used across application codeString keyEasy access, enumeration, and serialization
Metadata that should avoid string key collisionsLocal symbolIdentity separates the property from unrelated names
A shared extension key found through a known nameSymbol.for()Separate code can retrieve the registered identity
State restricted to a class bodyPrivate class fieldAccess syntax is restricted by the class definition
Associated state kept outside an objectWeakMapThe state is not stored as an own property
Arbitrary symbol values used as collection keysMapA map accepts symbols directly as keys

Symbols also support protocol extension. A well-known symbol lets an object participate in a language operation such as iteration without adding a guessed method name like "iterate". The next section implements that protocol.

Symbols are a poor fit when data must survive JSON serialization. They are also unnecessary when a plain string property is part of the intended public data model. Adding symbols to every internal field makes an object harder to inspect without providing access control.

For broader practice choosing data structures and handling language edge cases, the JavaScript coding interview guide provides a useful sequence. UIReady Premium Lifetime can help when a longer study plan with additional tested exercises fits the preparation schedule.

Well-Known Symbols in Real Code

A well-known symbol is a fixed protocol hook. JavaScript looks for that symbol during a corresponding language operation.

Symbol.iterator makes an object iterable

The iterable protocol looks up Symbol.iterator for a method. That method must return an iterator, which produces result objects containing value and done.

This complete implementation creates an inclusive numeric range:

function createRange(start, end) {
  return {
    start,
    end,

    [Symbol.iterator]() {
      let current = start;

      return {
        next() {
          if (current <= end) {
            return {
              value: current++,
              done: false,
            };
          }

          return {
            value: undefined,
            done: true,
          };
        },
      };
    },
  };
}

const range = createRange(3, 5);

console.log([...range]);
console.log(Array.from(range).join(":"));

const visited = [];
for (const value of range) {
  visited.push(value * 2);
}
console.log(visited);
[ 3, 4, 5 ]
3:4:5
[ 6, 8, 10 ]

Each call to range[Symbol.iterator]() creates a new iterator with its own current variable. That is why the same range can be consumed more than once.

One iterable, fresh state each timerange 3…5Symbol.iterator()iterator Acurrent starts at 3iterator Bcurrent starts at 33 → 4 → 5done3 → 4 → 5doneneither consumer advances the other consumer'scursor
A reusable iterable creates a fresh iterator for every consumer.

The iterator returns done: false with each yielded number. After the last number, it returns done: true. An interview implementation that returns the same exhausted iterator from every Symbol.iterator call may work once and then surprise the next loop.

Symbol.toPrimitive controls explicit conversion

An object can define Symbol.toPrimitive to respond to a conversion hint:

Conversion asks; the hook answersconversionoperationobject'sSymbol.toPrimitive(hint)string hintnumber/default hint$1212every branch must return a primitive, not anotherobject
Symbol.toPrimitive branches on the conversion hint.
const price = {
  amount: 12,

  [Symbol.toPrimitive](hint) {
    if (hint === "string") {
      return `$${this.amount}`;
    }

    return this.amount;
  },
};

console.log(Number(price));
console.log(String(price));
console.log(price + 3);
12
$12
15

The hook returns a primitive rather than another object. The conversion operation decides which hint it supplies.

Symbol.hasInstance customizes instanceof

The right-hand operand of instanceof can define Symbol.hasInstance:

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

console.log(4 instanceof EvenNumber);
console.log(5 instanceof EvenNumber);
true
false

This example is useful for output prediction. It also shows why instanceof should not automatically be read as a prototype check without inspecting the right-hand value.

instanceof can ask a custom questionvalue instanceof EvenNumberEvenNumber[Symbol.hasInstance]even integeranything elsetruefalsethe right-hand value owns the decision hook
A Symbol.hasInstance hook can replace the expected prototype test.

Symbol.toStringTag changes an object's tag

Symbol.toStringTag supplies the tag used by Object.prototype.toString:

const result = {
  [Symbol.toStringTag]: "QueryResult",
};

console.log(Object.prototype.toString.call(result));
[object QueryResult]

Other well-known symbols follow the same broad model: a language feature or built-in operation reads a fixed symbol property. Examples include hooks associated with asynchronous iteration, string matching operations, array species, and concatenation behavior. For an unfamiliar hook, identify the operation that reads it, the expected value at that property, and the required return contract.

JavaScript Symbol Interview Questions

These executable exercises combine identity, enumeration, copying, serialization, coercion, and iteration. For each one, predict the output before continuing, then paste the snippet into a browser console to test your prediction. The separately presented answer starts with the observable output and then explains the rule.

Do equal Symbol descriptions create equal values?

const a = Symbol("id");
const b = Symbol("id");

const object = {
  [a]: "first",
  [b]: "second",
};

console.log(a === b);
console.log(object[a]);
console.log(object[b]);
console.log(Object.getOwnPropertySymbols(object).length);
false
first
second
2

The description "id" does not establish identity. The object therefore has two separate symbol properties.

How do local and registered Symbols compare?

const local = Symbol("mode");
const registeredA = Symbol.for("mode");
const registeredB = Symbol.for("mode");

console.log(local === registeredA);
console.log(registeredA === registeredB);
console.log(Symbol.keyFor(local));
console.log(Symbol.keyFor(registeredA));
false
true
undefined
mode

The local symbol has a matching description but no registry membership. Both Symbol.for() calls use the same registry key and retrieve the same registered identity.

Which Symbol properties appear during enumeration and copying?

const marker = Symbol("marker");
const source = {
  visible: 1,
  [marker]: 2,
};

const keysFromLoop = [];
for (const key in source) {
  keysFromLoop.push(key);
}

const assigned = Object.assign({}, source);
const spread = { ...source };

console.log(Object.keys(source));
console.log(keysFromLoop);
console.log(Object.getOwnPropertySymbols(source).map(String));
console.log(assigned[marker]);
console.log(spread[marker]);
[ 'visible' ]
[ 'visible' ]
[ 'Symbol(marker)' ]
2
2

Object.keys() and for...in omit symbol keys. Object.assign() and object spread copy the enumerable own symbol property.

This is a common debugging trap. Code that uses Object.keys() to inspect a source can report fewer keys than a later copy operation actually transfers.

What does JSON.stringify do with Symbols?

const metadataKey = Symbol("metadata");
const value = Symbol("value");

const payload = {
  name: "test",
  savedValue: value,
  [metadataKey]: "metadata",
  list: [value],
};

console.log(JSON.stringify(payload));
{"name":"test","list":[null]}

The symbol-keyed property is ignored. The string-keyed savedValue property is omitted because its value is a symbol. The array retains its position and serializes the symbol element as null.

Which Symbol string conversions throw?

const label = Symbol("ready");

console.log(String(label));

try {
  console.log("" + label);
} catch (error) {
  console.log(error.name);
}

try {
  console.log(`${label}`);
} catch (error) {
  console.log(error.name);
}
Symbol(ready)
TypeError
TypeError

String(label) explicitly converts the symbol to text. Concatenation and template interpolation attempt conversions that throw for a symbol.

The conversion path mattersSymbol('ready')explicitimplicitimplicitString(label)succeeds'' + labelthrowstemplate textthrowsSymbol(ready)TypeErrorTypeErrorString() is a deliberate exception, not a generalcoercion rule
Explicit Symbol string conversion succeeds; implicit conversion throws.

Why does this computed property lookup fail?

const stateKey = Symbol("state");
const component = {
  [stateKey]: "mounted",
};

console.log(component[stateKey]);
console.log(component["state"]);
console.log(component.stateKey);
mounted
undefined
undefined

The object stores the property under the symbol identity. "state" and "stateKey" are string keys, so neither retrieves it. This is the same computed-key distinction that appears in many JavaScript interview questions.

What invariant does a reusable iterable need?

const sequence = {
  [Symbol.iterator]() {
    let value = 1;

    return {
      next() {
        if (value <= 2) {
          return {
            value: value++,
            done: false,
          };
        }

        return {
          value: undefined,
          done: true,
        };
      },
    };
  },
};

console.log([...sequence]);
console.log([...sequence]);
[ 1, 2 ]
[ 1, 2 ]

Each call to Symbol.iterator returns a fresh iterator. Both spreads therefore start at 1. Array spread consumes the iterable protocol, unlike object spread, which copies enumerable own properties.

Questions that combine these rules are good practice for the worked frontend interview examples and the broader frontend coding examples. The reliable approach is to identify the symbol category first, then ask whether the operation reads property keys, property values, or Symbol.iterator.

Frequently asked questions

What is a Symbol in JavaScript?
A Symbol is a primitive value that can act as an object property key. Each call to Symbol() creates a distinct identity, even when two symbols have the same description.
What is the difference between Symbol() and Symbol.for()?
Symbol() creates a new symbol on every call. Symbol.for() looks up a string key in the global symbol registry, returning the registered symbol when that key already exists.
Are Symbol properties private in JavaScript?
No. Common string enumeration methods skip symbol keys, but Object.getOwnPropertySymbols() and Reflect.ownKeys() expose them. Use private class fields or a WeakMap when access control matters.
Does JSON.stringify include Symbol properties?
JSON.stringify ignores symbol-keyed properties. It also omits symbol values stored in object properties, while symbol values inside arrays become null.