What is the difference between `Symbol('app.id')` and `Symbol.for('app.id')` in JavaScript?
answer
- one mints, one looks up
- a global table keyed by string
- same value across realms
- keyFor is the reverse lookup
- global namespace is back
basics
~20 sSymbol('app.id') creates a brand-new unique symbol every call. Symbol.for('app.id') looks the string up in a global registry shared across realms and returns the same symbol every time, so independent code can agree on one key.
solid answer
~40 s`Symbol('app.id')` mints a fresh, unique value each call — two calls are never equal. `Symbol.for('app.id')` consults the **global symbol registry**: the first call creates a symbol under that string key and every later call with the same string returns that same symbol, so `Symbol.for('a') === Symbol.for('a')` is `true`. The registry is shared across realms in the same agent — a page and its same-origin iframe, or two copies of a library bundled separately — which is exactly when you want it: independent code that must agree on one key without passing the symbol around. `Symbol.keyFor(sym)` returns the registry string for a registered symbol and `undefined` for a plain one. The trade-off is that a registry key is global namespace again, so prefix it (`'myapp.id'`) and accept that anyone can look it up.
go deeper
Remember the one-line difference: Symbol() makes a new unique value every time, while Symbol.for() returns the same shared symbol for a given string.
Explain the mechanism — a process-wide registry keyed by string, Symbol.keyFor as the reverse lookup, and the fact that the registry is shared across realms such as a page and its same-origin iframe.
Bring the real motivation: duplicate library instances or cross-realm code that must agree on a key without passing the symbol. Be explicit that this trades away collision-proofing and that registry entries are never released.
Treat a registry key as public API surface. Owning a namespaced string that other teams' code may look up means versioning it, documenting its meaning, and accepting that anything in the process can read or clobber the property it guards.
## Two opposite goals The plain `Symbol()` call exists to guarantee that *nobody else can have this value*. The registry exists to guarantee the opposite: that *everybody who asks by the same name gets the same value*. Both are useful; they answer different questions. ```js Symbol('a') === Symbol('a'); // false — uniqueness Symbol.for('a') === Symbol.for('a'); // true — shared identity ``` ## How the registry works `Symbol.for(key)` takes a string, coerces it to a string if it is not one, and looks it up in a single process-wide table the spec calls the **GlobalSymbolRegistry**. If an entry exists, the stored symbol is returned. If not, a new symbol is created with that string as both its registry key and its description, stored, and returned. There is no removal API — an entry lives for the life of the agent. `Symbol.keyFor` runs the lookup backwards: ```js const shared = Symbol.for('app.id'); const local = Symbol('app.id'); Symbol.keyFor(shared); // 'app.id' Symbol.keyFor(local); // undefined — not in the registry shared === local; // false — different values entirely shared.description; // 'app.id' ``` Note that `keyFor` returning `undefined` is the reliable way to tell a registered symbol from an unregistered one; the description alone tells you nothing, since a plain symbol can carry any description you like. ## Cross-realm is the point The registry is shared across **realms** — separate global environments that would otherwise each have their own copies of the built-ins. A page and a same-origin iframe are two realms; each has its own `Array`, its own `Object.prototype`, its own everything. Values created in one and passed to the other are still distinguishable (`arr instanceof Array` famously fails across an iframe boundary for exactly this reason). The symbol registry deliberately spans them: ```js // in the top page window.marker = Symbol.for('myapp.marker'); // in a same-origin iframe, independently Symbol.for('myapp.marker') === parent.marker; // true ``` The other everyday case has nothing to do with iframes: **two copies of the same library** ending up in one bundle, or a library loaded once as ESM and once as CommonJS. Two module instances each evaluate `const KEY = Symbol('lib.state')` and get *different* keys, so instance A cannot see the state instance B attached. `Symbol.for('lib.state')` makes them agree without either needing a reference to the other. Several real libraries use exactly this trick for singleton detection. ## What you give up A registry key is a **global string namespace**, with all the problems that implies: - Anyone in the process can call `Symbol.for('lib.state')` and get your key, read your property, and overwrite it. The collision-proofing that plain symbols give you is gone by design. - Two unrelated libraries can pick the same string, which is why the convention is to namespace: `'myapp.id'`, `'react.element'`-style prefixes. - Entries are never garbage-collected. That is a bounded leak (one symbol per distinct string) but a real one if you ever generate registry keys dynamically — never call `Symbol.for(userInput)` in a loop. ## Choosing between them Ask one question: *does some other code, which cannot receive this symbol as a value, need to name the same key?* - **No** — use `Symbol()`. This covers almost everything: private-ish metadata, per-module bookkeeping, keys you export from your own module so consumers import the value itself. - **Yes** — use `Symbol.for()` with a namespaced string. Cross-realm protocols, duplicate-instance detection, and conventions meant to be implemented by code you have never seen. A useful contrast: the *well-known* symbols such as `Symbol.iterator` are a third category. They are neither freshly minted nor in the registry — they are fixed properties of the `Symbol` constructor that the specification itself defines, and they are shared across realms because every realm's `Symbol.iterator` is the same value. `Symbol.keyFor(Symbol.iterator)` returns `undefined`, confirming they are not registry entries. ## Small details worth knowing - `Symbol.for(1)` works — the argument is converted to the string `'1'`. - The registry string becomes the description, so `Symbol.for('a').description === 'a'`. - There is no `Symbol.delete` or way to enumerate the registry from JavaScript.
- How can you tell at runtime whether a given symbol came from the registry?Call `Symbol.keyFor(sym)`. It returns the registry string for a symbol created by `Symbol.for`, and `undefined` for anything else — including a plain `Symbol('a')` with an identical description and including well-known symbols like `Symbol.iterator`. The description is not a reliable signal, since any symbol can carry any description.
- Why is `Symbol.for` the usual fix when two bundled copies of a library each think they are the only instance?Each copy evaluates its own `const KEY = Symbol('lib.state')` and gets a different value, so neither can see the property the other attached. `Symbol.for('lib.state')` resolves to one shared symbol regardless of how many module instances exist, so the second copy can detect the first's marker on a shared object and back off or warn.
- Is there any downside to using Symbol.for everywhere, since it seems strictly more capable?Yes — it throws away the property that makes symbols worth having. A registry key is a global string namespace: any code can call `Symbol.for` with the same string, read your property and overwrite it, and two libraries can collide on an unprefixed name. Registry entries are also never released. Use it only when unrelated code genuinely must name the same key.
saying these in an interview costs you the question
- Says Symbol.for('a') and Symbol('a') are the same value
- Thinks the registry is per-module or per-file
- Believes registry symbols can be deleted or enumerated
- Uses Symbol.for with unprefixed generic names like 'id'
- Calls Symbol.keyFor on a plain symbol and expects its description