Skip to content
JavaScript Basics: Use Symbol Keys without Mistaking Them for Privacy

JavaScript Basics: Use Symbol Keys without Mistaking Them for Privacy

October 21, 2026

String property names work well for everyday data. A Symbol gives a property a distinct identity when separate pieces of code need to attach information without accidentally choosing the same key. Its description is a label for people, not the identity itself.

These three independent examples were tested on Linux with Node.js v24.19.0. Built-in assertions check collisions, copying, and registry lookup. No packages, files beyond the examples, or network requests are needed.

Prepare a scratch directory

You need Bash, a text editor, Node.js 24 on your PATH, and the mktemp, rm, and rmdir utilities. Keep the same terminal open. Save each named source file as UTF-8 in this directory; .mjs selects ES modules. The setup stops if creating or entering the scratch directory fails.

node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

The tested runtime prints:

v24.19.0

1. Create different keys with the same description

Save this as distinct-symbol-keys.mjs:

import assert from 'node:assert/strict';

const firstStatus = Symbol('status');
const secondStatus = Symbol('status');
const job = {
  status: 'public',
  [firstStatus]: 'queued',
  [secondStatus]: 'reviewed',
};
assert.notEqual(firstStatus, secondStatus);
assert.equal(job[firstStatus], 'queued');
assert.equal(job[Symbol('status')], undefined);
assert.equal(job.status, 'public');
console.log(`String key: ${job.status}`);
console.log(`First symbol: ${job[firstStatus]}`);
console.log(`Second symbol: ${job[secondStatus]}`);

Run it:

node distinct-symbol-keys.mjs

Output:

String key: public
First symbol: queued
Second symbol: reviewed

Each Symbol('status') call creates a new symbol, even with identical descriptions. Brackets use the actual symbol as a property key. The ordinary string key status remains separate from both symbol keys.

Keep the created symbol somewhere you can reuse it. Reconstructing the description does not reconstruct the key, as the missing lookup demonstrates. Use Symbol() without new. The ECMAScript Symbol specification defines this identity behavior.

2. Inspect what enumeration and copying retain

Save this as inspect-symbol-keys.mjs:

import assert from 'node:assert/strict';

const review = Symbol('review');
const record = { title: 'Guide', [review]: 'checked' };
const copy = { ...record };
assert.deepEqual(Object.keys(record), ['title']);
assert.deepEqual(Object.getOwnPropertySymbols(record), [review]);
assert.deepEqual(Reflect.ownKeys(record), ['title', review]);
assert.equal(copy[review], 'checked');
assert.equal(JSON.stringify(record), '{"title":"Guide"}');
console.log(`Object.keys: ${Object.keys(record).join(', ')}`);
console.log(`Reflect.ownKeys: ${Reflect.ownKeys(record).map(String).join(', ')}`);
console.log(`Copied symbol value: ${copy[review]}`);
console.log(`JSON: ${JSON.stringify(record)}`);

Run it:

node inspect-symbol-keys.mjs

Output:

Object.keys: title
Reflect.ownKeys: title, Symbol(review)
Copied symbol value: checked
JSON: {"title":"Guide"}

Object.keys() lists enumerable own string keys. Object.getOwnPropertySymbols() finds own symbols, and Reflect.ownKeys() includes both kinds, even non-enumerable keys. Symbols are therefore discoverable; they do not protect secrets. See the Reflect.ownKeys reference.

Object spread copies this enumerable symbol property, while JSON serialization omits symbol-keyed properties. Do not rely on JSON to preserve an in-memory symbol annotation. A copied property is also different from a private field: anyone who obtains the symbol, including through reflection, can use it. Choose explicit string-keyed data when a value must cross a JSON boundary.

3. Share a deliberate registry key

Save this as registered-symbol-key.mjs:

import assert from 'node:assert/strict';

const writerKey = Symbol.for('tutorial.review-status');
const readerKey = Symbol.for('tutorial.review-status');
const localKey = Symbol('tutorial.review-status');
const record = { [writerKey]: 'approved' };

assert.equal(writerKey, readerKey);
assert.notEqual(writerKey, localKey);
assert.equal(record[readerKey], 'approved');
assert.equal(Symbol.keyFor(writerKey), 'tutorial.review-status');
assert.equal(Symbol.keyFor(localKey), undefined);
assert.throws(() => Symbol.keyFor('tutorial.review-status'), TypeError);
console.log(`Shared value: ${record[readerKey]}`);
console.log(`Registry name: ${Symbol.keyFor(writerKey)}`);
console.log(`Local registry name: ${Symbol.keyFor(localKey)}`);

Run it:

node registered-symbol-key.mjs

Output:

Shared value: approved
Registry name: tutorial.review-status
Local registry name: undefined

Symbol.for() looks up a string in the global symbol registry and creates an entry only when needed. Code using that same registry name gets the same symbol. Symbol.keyFor() retrieves a registered symbol’s name, but returns undefined for an ordinary local symbol, even when its description matches.

Registry names intentionally permit agreement, so use a distinctive namespace and document its purpose. They are not a privacy boundary or a mechanism for exchanging an object between separate processes. For a module-internal annotation, a local symbol is usually simpler. The Symbol reference distinguishes local symbols, registered symbols, and well-known symbols used by language protocols.

Clean up

In the same terminal, remove only the named files, return to the original directory, and remove the empty scratch directory:

rm -- distinct-symbol-keys.mjs inspect-symbol-keys.mjs registered-symbol-key.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Acknowledgements

This post was created with assistance from AI.

Last updated on