BigInt JSON.stringify — The Hidden Serialization Bug
JSON.
- Symbol creates guaranteed-unique property keys, even when strings collide
- BigInt handles integers beyond Number.MAX_SAFE_INTEGER (2^53-1) without precision loss
- Well-known Symbols like Symbol.iterator enable for…of loops and protocol hooks
- BigInt operations with regular Number throw TypeError — mixed math is silent failure
- JSON.stringify silently drops BigInt values — use custom replacer in production
Imagine every locker in a school has a name tag. Two kids named 'Alex' might grab the wrong locker — that's a naming collision. Symbol gives every locker a truly one-of-a-kind invisible serial number, so no two ever clash, even if they share the same label. BigInt is like a calculator that never runs out of digits — regular JavaScript numbers max out around 9 quadrillion, but BigInt can handle numbers as large as your RAM allows, perfect for cryptography or working with 64-bit IDs from databases.
Most JavaScript developers spend years writing production code without ever reaching for Symbol or BigInt — and that's precisely why they get burned when they finally need them. Symbol quietly underpins how JavaScript's own built-in iteration protocol works (yes, Symbol.iterator is literally how for...of loops function). BigInt exists because JavaScript's IEEE 754 double-precision floats silently lose precision on integers above 2^53 - 1, which is exactly the size of the IDs Twitter's Snowflake algorithm generates. Both primitives arrived in ES2015 and ES2020 respectively, solving problems that had been causing silent bugs in production for years.
The problem Symbol solves is property key uniqueness and metaprogramming hooks. When you're building a shared library or extending third-party objects, string keys collide. Two independent plugins both adding a 'validate' key to an object will silently overwrite each other. Symbol makes that impossible because every Symbol() call returns a value that is guaranteed unequal to every other value in the universe. BigInt solves numeric precision: the moment you receive a 64-bit integer from a REST API, a blockchain transaction amount, or a database row ID, JavaScript's Number type may already be lying to you about its value.
By the end of this article you'll understand exactly when Symbol and BigInt should be your first choice — not an afterthought. You'll know how Symbol powers the JavaScript iteration and reflection protocols, how to create truly private-ish object properties, when BigInt causes silent type errors, and how to write defensive code that handles both primitives safely in real applications.
What is Symbol and BigInt in JavaScript?
Symbol and BigInt are the two primitives added to JavaScript after the original six. Symbol, from ES2015, is a unique and immutable value used primarily as object property keys. BigInt, from ES2020, represents integers of arbitrary precision — it can hold values larger than 2^53-1 without rounding.
Let's see them in action. The code below creates a Symbol and a BigInt, uses a Symbol as a property key, and demonstrates BigInt arithmetic.
- Every
Symbol()call creates a new, never-before-seen value. - Same Symbol description does not imply equality — they are distinct.
- String keys are like name tags — duplicates cause collisions.
- Symbol keys are like serial numbers — always unique.
- BigInt is like a ruler that never ends — it doesn't round.
Symbol() call as an entirely new identity.Symbol.for() only when cross-module sharing is intended.Well-Known Symbols: How JavaScript Uses Symbol Internally
JavaScript itself uses Symbol for a dozen built-in protocols. These are accessed via Symbol.iterator, Symbol.hasInstance, Symbol.toStringTag, Symbol.match, and more. They allow user-defined objects to integrate seamlessly with language features.
For example, Symbol.iterator is the property that makes an object iterable. When you write for (const x of obj), JavaScript looks for obj[Symbol.iterator]. If it finds a function, it calls it to get an iterator. The same mechanism works with spread syntax (...) and destructuring.
Another one: Symbol.toPrimitive lets you control how an object is converted to a primitive — useful when you want custom coercion logic for BigInt or Number interactions.
BigInt Arithmetic and Type Mixing: The Silent Failure Trap
BigInt supports all standard arithmetic operators: +, -, , /, %, and *. But there's a hard rule: you cannot mix BigInt and Number in the same expression without explicit conversion. Doing so throws a TypeError immediately.
That seems straightforward — until you debug production code where a database query returns a mix of BigInt IDs and Number counts. The error is thrown at the point of operation, but the root cause may be far upstream where a property was expected to be a number but was actually a BigInt.
Division is another trap. BigInt division truncates toward zero — there is no fractional part. 5n / 2n is 2n, not 2.5. If you need fractions, convert to Number or use a decimal library.
Comparisons (==, <, >) between BigInt and Number work and return the expected boolean — but beware of == vs === . 1n == 1 is true, but 1n === 1 is false because the types differ.
Symbol Registry and Shared Symbols Across Modules
By default, each Symbol() call creates a brand new symbol. But what if you need the same symbol across multiple modules or files? Use Symbol.for(key) — it creates a symbol that is stored in a global registry. Calling Symbol.for('app.id') in two different modules returns the exact same symbol.
This is useful for well-known protocols, plugin systems, or cross-cutting metadata keys. However, it also introduces coupling: any module can access or overwrite the key's mapping. Use the registry deliberately, never by accident.
Symbol.keyFor(symbol) returns the key string for a registered symbol, which helps debugging but also exposes the key. Don't rely on it for security.
Symbol() for truly private-like keys internal to a single module. Use Symbol.for() when you need a well-known constant shared across your application — like plugin hooks or event types.Symbol.for() is global — any code can collide with your key.Symbol() gives local uniqueness.Symbol.for() gives global uniqueness.Serializing Symbol and BigInt: JSON, structuredClone, and Beyond
Symbol-keyed properties are invisible to JSON.stringify — they are simply omitted. BigInt values cause JSON.stringify to throw a TypeError unless a replacer function is provided. This is a common source of silent data loss in APIs.
The structuredClone() API (available in modern browsers and Node.js 17+) handles Symbols by throwing a DOMException. It cannot clone Symbol-keyed properties or BigInt values. If you need to clone objects containing these types, implement a custom deep clone or use libraries like lodash's cloneDeep with a customizer.
For safe serialization, always provide a toJSON method on BigInt and ensure your JSON replacer handles both Symbol and BigInt appropriately. For Symbol keys, consider converting them to strings with a prefix like '__sym_' before serialization, and restore them on read.
The Hidden Serialization Bug: BigInt Lost in JSON
function() { return this.toString(); }.- Never assume JSON.stringify handles non-standard primitives — test serialization paths with real data.
- Add custom toJSON to BigInt if your app passes it through JSON anywhere.
- Add a linter rule to flag BigInt properties used in API responses without explicit serialization.
Object.keys() does not return Symbol-keyed propertiesfunction() { return this.toString(); } before any serialization. Verify with a test that logs the stringified output.Key takeaways
Symbol() call is distinct, Symbol.for() shares across modules.Common mistakes to avoid
5 patternsAssuming Symbol('desc') === Symbol('desc')
Symbol.for() if the symbol needs to be shared across modules by key.Using BigInt in JSON.stringify without toJSON
function() { return this.toString(); }; early in application startup. Optionally add a JSON.stringify replacer for custom handling.Mixing BigInt and Number without explicit conversion
Expecting Object.keys or for…in to reveal Symbol keys
Using structuredClone on objects with BigInt or Symbol
Interview Questions on This Topic
What is a Symbol in JavaScript? How is it different from a string key?
Symbol() — every call returns a unique, immutable value. Unlike string keys, Symbol keys are guaranteed not to collide with any other key, even if the same description is used. Symbols are not enumerable in for…in loops, and they are hidden from Object.keys and JSON.stringify. Use them for metaprogramming, well-known protocols (Symbol.iterator), and adding metadata to objects without risk of name clashes.Frequently Asked Questions
That's Advanced JS. Mark it forged?
4 min read · try the examples if you haven't