Mid-level 4 min · March 05, 2026

BigInt JSON.stringify — The Hidden Serialization Bug

JSON.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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
Plain-English First

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.

ForgeExample.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// TheCodeForge — Symbol and BigInt in JavaScript example

// Symbol: unique every time
const sym1 = Symbol('debug');
const sym2 = Symbol('debug');
console.log(sym1 === sym2); // false

// Symbol as object key
const obj = { [sym1]: 'hidden value' };
console.log(obj[sym1]); // 'hidden value'
console.log(obj.sym1); // undefined — string key, not Symbol

// BigInt: arbitrary precision
const big = 1234567890123456789012345678901234567890n;
console.log(big * 2n); // 2469135780246913578024691357802469135780n

// BigInt + Number throws
// console.log(big + 1); // TypeError!
Output
false
hidden value
undefined
2469135780246913578024691357802469135780n
The Unique Lock Analogy
  • 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.
Production Insight
You might think Symbol('debug') === Symbol('debug') — it's not.
Treat every Symbol() call as an entirely new identity.
Rule: use global registry Symbol.for() only when cross-module sharing is intended.
Key Takeaway
Symbol gives you collision-free keys.
BigInt gives you exact integers beyond 2^53.
Both are primitives — but behave differently from strings and numbers.
Should I Use Symbol or String for Property Keys?
IfProperty needs to be truly private from other code?
UseUse Symbol — but remember it's still discoverable via getOwnPropertySymbols. For true privacy, use WeakMap or private fields (#).
IfNeed to avoid collisions in a library?
UseUse Symbol. Each symbol is unique per call, so no two libraries overwrite each other.
IfProperty must be enumerable or serializable?
UseUse string keys. Symbols are hidden from for…in, Object.keys, JSON.stringify.
IfNeed to implement custom iteration or introspection?
UseUse well-known Symbols like Symbol.iterator, Symbol.hasInstance, Symbol.toStringTag.

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.

ForgeExample.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// TheCodeForge — Well-Known Symbols in action

// Custom iterable with Symbol.iterator
const range = {
  start: 1,
  end: 5,
  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        }
        return { done: true };
      }
    };
  }
};

for (const n of range) {
  console.log(n); // 1 2 3 4 5
}

// Symbol.toPrimitive for custom coercion
const accountId = {
  value: 12345678901234567890n,
  [Symbol.toPrimitive](hint) {
    if (hint === 'string') return this.value.toString();
    if (hint === 'number') return Number(this.value); // warning: may lose precision
    return this.value;
  }
};
console.log(`Account: ${accountId}`); // 'Account: 12345678901234567890'
Output
1
2
3
4
5
Account: 12345678901234567890
Production Note: Custom Iterable with BigInt
If your range uses BigInt values, ensure the iterator returns BigInt types consistently. Mixing Number and BigInt in iteration can lead to type errors when operations are performed.
Production Insight
Overriding Symbol.toPrimitive can hide precision loss.
When you convert a BigInt to Number via toPrimitive with hint 'number', you silently lose bits beyond 53.
Rule: never coerce BigInt to Number in production unless you've validated the range.
Key Takeaway
Well-known Symbols are the hooks into JavaScript's runtime.
Symbol.iterator makes any object iterable.
Symbol.toPrimitive controls coercion — use it with care.

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.

ForgeExample.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// TheCodeForge — BigInt arithmetic pitfalls

// Division truncation
console.log(5n / 2n); // 2n, not 2.5

// Type mixing throws
let id = 12345678901234567890n;
let count = 5;
// console.log(id + count); // TypeError: Cannot mix BigInt and other types

// Safe conversion with range check
function safeAddToBigInt(big, num) {
  if (typeof num !== 'number' || num > Number.MAX_SAFE_INTEGER) {
    throw new Error('Unsafe conversion or type mismatch');
  }
  return big + BigInt(num);
}
console.log(safeAddToBigInt(id, count)); // 12345678901234567895n

// Comparison quirks
console.log(1n == 1);  // true (abstract equality)
console.log(1n === 1); // false (strict equality)

// Ordering works
console.log(2n > 1); // true
Output
2n
12345678901234567895n
true
false
true
Production Warning: Implicit Coercion in APIs
When a REST API returns a number that looks like an integer, it's parsed as JavaScript Number if the field is not quoted. Use a custom JSON reviver to parse large numbers as BigInts: JSON.parse(text, (key, value) => typeof value === 'number' && !Number.isSafeInteger(value) ? BigInt(value) : value).
Production Insight
Division truncation catches many teams off-guard.
A report that expects 5.0 instead shows 2 because of BigInt division.
Rule: always convert to Number after division if you need fractional precision.
Key Takeaway
BigInt + Number throws — convert explicitly.
BigInt division truncates — not a surprise if you know.
Use BigInt for integers over 2^53 — use Number for everything else.

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.

ForgeExample.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// TheCodeForge — Symbol registry sharing

// Module A
const APP_META = Symbol.for('app.meta');
obj[APP_META] = { version: '2.0.1', author: 'TheCodeForge' };

// Module B (different file)
const APP_META = Symbol.for('app.meta');
console.log(obj[APP_META]); // { version: '2.0.1', author: 'TheCodeForge' }

// Retrieve key string
console.log(Symbol.keyFor(APP_META)); // 'app.meta'

// Non-registered symbol returns undefined
const local = Symbol('local');
console.log(Symbol.keyFor(local)); // undefined
Output
{ version: '2.0.1', author: 'TheCodeForge' }
app.meta
undefined
When to Use Symbol.for() vs Symbol()
Use 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.
Production Insight
Symbol.for() is global — any code can collide with your key.
A third-party script could overwrite a Symbol.for('app.meta') if unaware.
Rule: namespace your registry keys like fully qualified names: 'com.thecodeforge.app.meta'.
Key Takeaway
Symbol() gives local uniqueness.
Symbol.for() gives global uniqueness.
Namespace registry keys to avoid collisions across libraries.

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.

ForgeExample.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// TheCodeForge — Safe serialization

// BigInt toJSON
BigInt.prototype.toJSON = function() { return this.toString(); };

const data = {
  id: 9876543210123456789n,
  [Symbol('private')]: 'hidden'
};

// Without replacer, Symbol key is lost but BigInt works
console.log(JSON.stringify(data)); // {"id":"9876543210123456789"}

// Custom replacer to preserve Symbol keys (as string)
function replacer(key, value) {
  if (typeof key === 'symbol') {
    this[`__sym_${key.description}`] = value;
    return;
  }
  return value;
}

// structuredClone throws for Symbol/BigInt
try {
  const clone = structuredClone(data);
} catch (e) {
  console.log(e.name); // DOMException
}
Output
{"id":"9876543210123456789"}
DOMException
Production Pitfall: structuredClone and BigInt
structuredClone is not a safe way to deep clone objects that may contain BigInt or Symbol properties. It throws at the first encounter. Use a custom clone function or a well-tested library instead.
Production Insight
Many teams discover BigInt JSON issues only after a production outage.
A missing toJSON causes the entire JSON.stringify to throw, not just skip the value.
Rule: always override BigInt.prototype.toJSON in your app bootstrap if you use BigInt in API responses.
Key Takeaway
JSON.stringify omits Symbol keys and throws on BigInt.
Add BigInt.prototype.toJSON to prevent serialization crashes.
structuredClone cannot handle BigInt or Symbol at all.
● Production incidentPOST-MORTEMseverity: high

The Hidden Serialization Bug: BigInt Lost in JSON

Symptom
Server received transactions but downstream Kafka consumers failed to match orders. Logs showed id fields were missing, but code looked correct.
Assumption
The team assumed JSON.stringify would serialize all object properties, including BigInt values stored as database IDs.
Root cause
JSON.stringify throws a TypeError for BigInt values by default — it doesn't silently skip them. The catch block was swallowing the error, causing the property to be omitted entirely.
Fix
Replaced BigInt serialization with a custom JSON.stringify replacer: (key, value) => typeof value === 'bigint' ? value.toString() : value. Also added a toJSON method to the BigInt prototype: BigInt.prototype.toJSON = function() { return this.toString(); }.
Key lesson
  • 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.
Production debug guideSymptom → Action mapping for the traps engineers hit most often4 entries
Symptom · 01
Object.keys() does not return Symbol-keyed properties
Fix
Use Object.getOwnPropertySymbols(obj) to retrieve Symbol keys, or Reflect.ownKeys(obj) to get both string and Symbol keys.
Symptom · 02
BigInt + Number throws TypeError: Cannot mix BigInt and other types
Fix
Explicitly convert: Number(bigInt) if safe, or BigInt(number). Wrap in try/catch with a range check: if (number > Number.MAX_SAFE_INTEGER) throw new Error('Loss of precision').
Symptom · 03
JSON.stringify silently removes BigInt fields
Fix
Add BigInt.prototype.toJSON = function() { return this.toString(); } before any serialization. Verify with a test that logs the stringified output.
Symptom · 04
Symbol properties are not enumerable in for…in loops
Fix
Use for…of with Object.getOwnPropertySymbols(obj) or Reflect.ownKeys to iterate all own property keys.
★ Symbol & BigInt Debug Cheat SheetCommon symptoms and immediate commands to diagnose production issues with Symbol and BigInt.
Symbol-keyed properties invisible in console.log(obj)
Immediate action
Check if console displays Symbol keys — many devtools show them in a separate section
Commands
console.log(Object.getOwnPropertySymbols(obj))
console.log(Reflect.ownKeys(obj))
Fix now
Use Reflect.ownKeys to get all own keys for debugging; store Symbol in a variable for reliable access.
BigInt operation throws TypeError on mixed types+
Immediate action
Identify which operand is BigInt vs Number — use typeof in logs
Commands
console.log('a:', typeof a, 'b:', typeof b)
if (typeof a !== typeof b) { /* convert explicitly */ }
Fix now
Use BigInt(number) or Number(bigInt) after checking range, or wrap arithmetic in a function that normalizes types.
BigInt values missing in API response JSON+
Immediate action
Check if BigInt.prototype.toJSON is defined — if not, JSON.stringify throws
Commands
console.log(BigInt.prototype.toJSON)
const json = JSON.stringify(obj, (key, value) => typeof value === 'bigint' ? value.toString() : value)
Fix now
Add BigInt.prototype.toJSON = function() { return this.toString(); } globally in your app entry point.
Symbol vs BigInt Quick Reference
FeatureSymbolBigInt
Introduced inES2015 (ES6)ES2020 (ES11)
PurposeUnique property keys & metaprogramming hooksArbitrary precision integers
Type detectiontypeof sym === 'symbol'typeof big === 'bigint'
Equality uniquenessSymbol() !== Symbol()2n === 2n (value equality)
Mixing with NumberN/A (Symbol doesn't mix)Throws TypeError — must convert explicitly
JSON.stringify behaviorSymbol keys omitted silentlyThrows TypeError unless toJSON defined
Can be global?Yes, via Symbol.for(key)No registry — always literal value
Common use caseCustom iterators, private-like properties, hooksCrypto, DB IDs, blockchain, timestamps

Key takeaways

1
Symbol provides truly unique property keys
every Symbol() call is distinct, Symbol.for() shares across modules.
2
BigInt handles integers beyond Number.MAX_SAFE_INTEGER without rounding, but requires explicit conversion when mixed with Number.
3
JSON.stringify drops Symbol keys and throws on BigInt
always add BigInt.prototype.toJSON in production apps.
4
Well-known Symbols (Symbol.iterator, Symbol.hasInstance, etc.) let you tap into JavaScript's built-in behaviors.
5
Division with BigInt truncates toward zero
use Number conversion if you need fractional results.
6
structuredClone cannot handle BigInt or Symbol
implement a custom deep clone for objects containing them.

Common mistakes to avoid

5 patterns
×

Assuming Symbol('desc') === Symbol('desc')

Symptom
Library code tries to access a Symbol-keyed property by creating a new Symbol with the same description, but it misses the property entirely.
Fix
Store the Symbol in a variable or export it. Use Symbol.for() if the symbol needs to be shared across modules by key.
×

Using BigInt in JSON.stringify without toJSON

Symptom
JSON.stringify throws TypeError: Do not know how to serialize a BigInt. Entire API response fails, often caught only in staging after deploy.
Fix
Add BigInt.prototype.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

Symptom
Code like bigInt + 5 throws TypeError at runtime. Hard to debug because the error bubbles up from deep inside an expression.
Fix
Wrap mixed-type operations in a helper that validates and converts: const safeAdd = (a, b) => typeof a === 'bigint' ? a + BigInt(b) : BigInt(a) + b;
×

Expecting Object.keys or for…in to reveal Symbol keys

Symptom
Objects with Symbol keys appear empty when enumerated with standard loops, leading to missing data in form rendering or data exports.
Fix
Use Object.getOwnPropertySymbols(obj) or Reflect.ownKeys(obj) to retrieve all own property keys including Symbols.
×

Using structuredClone on objects with BigInt or Symbol

Symptom
structuredClone throws a DOMException, stopping deep copy logic that works for other data types.
Fix
Implement a custom deep clone that handles BigInt by converting to string and reconstructing, and either skips Symbol keys or strings them with a prefix.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is a Symbol in JavaScript? How is it different from a string key?
Q02SENIOR
Explain the difference between Symbol() and Symbol.for(). When would you...
Q03SENIOR
Why can't you mix BigInt and Number in the same expression? How do you h...
Q04SENIOR
What happens when you try to JSON.stringify an object with a BigInt prop...
Q01 of 04JUNIOR

What is a Symbol in JavaScript? How is it different from a string key?

ANSWER
A Symbol is a primitive value created by calling 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.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is Symbol and BigInt in JavaScript in simple terms?
02
Can Symbol keys be accessed by string methods like Object.keys?
03
How do I check if a value is a Symbol or BigInt?
04
Is there a maximum size for BigInt?
05
Can I use Symbol as a key in a Map or Set?
🔥

That's Advanced JS. Mark it forged?

4 min read · try the examples if you haven't

Previous
Modules in JavaScript — import export
11 / 27 · Advanced JS
Next
WeakMap and WeakSet in JavaScript