JSON.stringify() Edge Cases — undefined, null, Dates, Circular References, BigInt & More
JSON.stringify() seems simple until it silently drops data you expected, throws on inputs that look fine, or produces output that does not round-trip back correctly. This guide covers every edge case developers actually hit: undefined vs null, Date serialization, circular references, BigInt, NaN, Infinity, functions, Symbols, Map, Set, and how to handle each one correctly.
silent
undefined in object properties — silently dropped with no warning
throws
Circular references and BigInt — TypeError at runtime
null
NaN, Infinity, -Infinity — all become null in JSON output
undefined — The Silent Data Killer
The most common JSON.stringify() bug: undefined silently disappears from object properties. No error, no warning — the key is simply absent in the output. This causes real bugs when sending PATCH requests, logging state, or saving to storage.
// In object properties: undefined is OMITTED (key disappears)
JSON.stringify({ a: 1, b: undefined, c: 3 });
// '{"a":1,"c":3}' ← b is gone
// In arrays: undefined becomes null (index is preserved)
JSON.stringify([1, undefined, 3]);
// '[1,null,3]' ← null preserves the array position
// At top level: returns the JS value undefined (not a string!)
JSON.stringify(undefined);
// undefined ← not the string "undefined"
typeof JSON.stringify(undefined); // 'undefined'
// Functions — same as undefined in all positions
JSON.stringify({ fn: () => 'hello', name: 'Alice' });
// '{"name":"Alice"}' ← fn silently omitted
JSON.stringify([() => 'a', 'b', () => 'c']);
// '[null,"b",null]' ← functions become null in arraysSilent drops cause PATCH vs PUT semantic bugs
JSON.stringify(data) and data.field is undefined, the field is absent from the request body. The server-side handler never sees the field and leaves it unchanged. This is different from passing null, which tells the server to explicitly clear the value. Always use null for intentionally absent fields.undefined silently removed from PATCH payload
undefined omitted — server keeps old value (wrong!)
// User cleared the phone field — you want to remove it server-side
const updates = {
name: 'Alice Smith',
phone: undefined, // user removed their phone number
email: 'alice@new.com',
};
const body = JSON.stringify(updates);
// '{"name":"Alice Smith","email":"alice@new.com"}'
// phone field is ABSENT — server keeps the old phone number unchanged!null sent — server explicitly clears the field
// Use null to explicitly clear a value server-side
const updates = {
name: 'Alice Smith',
phone: null, // explicit: clear this field
email: 'alice@new.com',
};
JSON.stringify(updates);
// '{"name":"Alice Smith","phone":null,"email":"alice@new.com"}'
// Server sees phone: null and clears it
// Or: convert all undefined to null with a replacer
JSON.stringify(updates, (key, val) => (val === undefined ? null : val));null — The Safe Stand-In for Absent Values
Unlike undefined, null is a valid JSON value. It is preserved in objects, arrays, and as a top-level value. Use null whenever you need to represent an intentionally absent or cleared value.
// null is preserved everywhere
JSON.stringify({ a: null, b: null, c: 3 });
// '{"a":null,"b":null,"c":3}' ← null is kept
JSON.stringify([1, null, 3]);
// '[1,null,3]' ← null in arrays also preserved
JSON.stringify(null);
// 'null' ← valid JSON string!
// null is the correct JSON "no value" type
// Parsing null back gives null (round-trips perfectly)
JSON.parse(JSON.stringify(null)); // nullDate Objects — Automatic ISO 8601 Conversion
Date objects are not valid JSON — but they have a toJSON()method that returns an ISO 8601 string. JSON.stringify() calls toJSON() automatically, so Dates serialize to readable timestamp strings. The round-trip is not automatic — you get a string back, not a Date.
const event = {
title: 'Team Meeting',
start: new Date('2024-11-15T09:00:00Z'),
end: new Date('2024-11-15T10:30:00Z'),
created: new Date(),
};
const json = JSON.stringify(event, null, 2);
// {
// "title": "Team Meeting",
// "start": "2024-11-15T09:00:00.000Z",
// "end": "2024-11-15T10:30:00.000Z",
// "created": "2024-11-15T14:22:33.456Z"
// }
// Dates are automatically converted to ISO 8601 strings
// Round-trip: parsing gives back strings, not Date objects!
const parsed = JSON.parse(json);
parsed.start; // '2024-11-15T09:00:00.000Z' — a string
parsed.start instanceof Date; // false
// Re-construct dates after parsing
const restored = {
...parsed,
start: new Date(parsed.start),
end: new Date(parsed.end),
created: new Date(parsed.created),
};
restored.start instanceof Date; // trueDate.toJSON() is called before replacer
The toJSON() method is called before the replacer function runs. So in a replacer, value for a Date property will already be an ISO string, not a Date object. Check typeof value === 'string' and validate with Date.parse() if you need to detect serialized dates in a replacer.
NaN, Infinity, -Infinity — All Become null
IEEE 754 special numbers — NaN, Infinity, and -Infinity — have no JSON representation. JSON.stringify() converts all three to null. This is a silent lossy conversion that can corrupt numerical data.
// All three become null — silently
JSON.stringify({ a: NaN, b: Infinity, c: -Infinity });
// '{"a":null,"b":null,"c":null}'
JSON.stringify([NaN, Infinity, -Infinity, 42]);
// '[null,null,null,42]'
// This is particularly dangerous in calculated fields
const stats = {
mean: 100,
stddev: 0 / 0, // division by zero → NaN
max: Infinity,
min: 10,
};
JSON.stringify(stats);
// '{"mean":100,"stddev":null,"max":null,"min":10}'
// stddev and max silently corruptedNaN from division or failed parse silently becomes null in JSON
NaN silently becomes null — server receives wrong type
const score = parseInt('not a number'); // NaN
const stats = { userId: 42, score };
JSON.stringify(stats);
// '{"userId":42,"score":null}'
// score is null — not NaN — in the API payload
// Server receives null and may store it as 0 or null, not as an errorValidate numbers before serialize — no silent corruption
// Validate before serializing
const rawScore = parseInt('not a number');
const score = Number.isFinite(rawScore) ? rawScore : 0; // fallback to 0
const stats = { userId: 42, score };
JSON.stringify(stats);
// '{"userId":42,"score":0}'
// Or: use replacer to catch all NaN/Infinity
JSON.stringify(stats, (key, val) => {
if (typeof val === 'number' && !isFinite(val)) return null; // or 0, or throw
return val;
});Circular References — TypeError at Runtime
If an object references itself (directly or through a chain), JSON.stringify() throwsTypeError: Converting circular structure to JSON. This happens with DOM nodes, error objects, and manually constructed trees.
// Direct circular reference
const obj = { name: 'Alice' };
obj.self = obj; // obj.self points back to obj
JSON.stringify(obj);
// TypeError: Converting circular structure to JSON
// Indirect circular reference
const parent = { name: 'Parent' };
const child = { name: 'Child', parent };
parent.child = child; // parent → child → parent → ...
JSON.stringify(parent);
// TypeError: Converting circular structure to JSON
// Common sources of circular references:
// - DOM nodes (node.parentNode, node.childNodes)
// - Express req/res objects
// - Error objects with .cause chains
// - Custom linked list/tree nodes// Approach 1: replacer that tracks seen objects
function safeStringify(obj, space) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) return '[Circular]';
seen.add(value);
}
return value;
}, space);
}
const obj = { name: 'Alice' };
obj.self = obj;
safeStringify(obj);
// '{"name":"Alice","self":"[Circular]"}'
// Approach 2: install the 'flatted' package (drop-in replacement)
import { stringify, parse } from 'flatted';
stringify(circularObj); // handles circular structures natively
// Approach 3: restructure the data before serializing
// If you control the data model, avoid circular refs from the startBigInt — Throws TypeError
JSON.stringify() throws TypeError: Do not know how to serialize a BigInt when it encounters a BigInt value. Convert BigInt to string or number before serializing.
// BigInt throws
JSON.stringify({ id: 9007199254740993n });
// TypeError: Do not know how to serialize a BigInt
// Fix 1: convert to string (safest — no precision loss)
JSON.stringify({ id: 9007199254740993n.toString() });
// '{"id":"9007199254740993"}'
// Fix 2: convert to number (only if value fits safely in Number)
// Number.MAX_SAFE_INTEGER = 9007199254740991
const safe = 9007199254740991n;
JSON.stringify({ id: Number(safe) });
// '{"id":9007199254740991}'
// Fix 3: replacer to auto-convert all BigInt to strings
JSON.stringify(data, (key, val) => {
if (typeof val === 'bigint') return val.toString();
return val;
});
// Fix 4: add BigInt.prototype.toJSON (affects all BigInt globally)
BigInt.prototype.toJSON = function() { return this.toString(); };
JSON.stringify({ id: 42n }); // '{"id":"42"}'Map and Set — Become Empty Objects
Map and Set are not JSON-serializable types. JSON.stringify() does not throw — it silently converts them to empty objects {}, losing all data.
// Map → empty object (silent data loss!)
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
JSON.stringify(map);
// '{}' ← all data lost, no error!
// Set → empty object (silent data loss!)
const set = new Set([1, 2, 3, 4]);
JSON.stringify(set);
// '{}' ← all data lost, no error!
// Fix for Map: convert to array of entries or plain object first
JSON.stringify([...map.entries()]);
// '[["a",1],["b",2],["c",3]]'
JSON.stringify(Object.fromEntries(map));
// '{"a":1,"b":2,"c":3}'
// Fix for Set: convert to array
JSON.stringify([...set]);
// '[1,2,3,4]'
// Round-trip: restore after parsing
const entries = JSON.parse(JSON.stringify([...map.entries()]));
const restoredMap = new Map(entries);Symbol — Always Omitted
const sym = Symbol('id');
// Symbol-keyed properties are omitted
const obj = { [sym]: 42, name: 'Alice' };
JSON.stringify(obj);
// '{"name":"Alice"}' ← sym key omitted
// Symbol values in objects are omitted
JSON.stringify({ key: Symbol('value'), name: 'Alice' });
// '{"name":"Alice"}' ← Symbol value omitted like undefined
// Symbol values in arrays become null
JSON.stringify([Symbol('a'), 'b', Symbol('c')]);
// '[null,"b",null]'
// Symbol properties are fundamentally non-enumerable in JSON context
// Use string keys for data you need to serializeQuick Reference — All Special Cases
undefined (object prop) → omitted
Key silently disappears. Fix: use null instead, or replacer: (k,v) => v === undefined ? null : v
undefined (array element) → null
Index preserved as null. JSON has no undefined type — null is the closest equivalent.
Function (object prop) → omitted
Functions have no JSON representation. Silently dropped. Use toJSON() or build a plain data object.
Symbol (object prop) → omitted
Both Symbol keys and Symbol values in objects are omitted. In arrays, Symbol values become null.
Date → ISO 8601 string
Date.toJSON() produces "2024-11-15T10:30:00.000Z". Parsing gives back a string — re-construct with new Date(str).
NaN, Infinity, -Infinity → null
Silent lossy conversion. Validate numbers before serializing: if (!Number.isFinite(val)) throw or substitute.
Circular reference → TypeError
Throws at runtime. Fix: replacer with WeakSet to track seen objects, or use the flatted package.
BigInt → TypeError
Throws at runtime. Fix: convert to string (bigInt.toString()) or number (Number(bigInt)) before serializing.
Map → empty object {}
Silent data loss. Fix: Object.fromEntries(map) or [...map.entries()] before stringifying.
Set → empty object {}
Silent data loss. Fix: [...set] or Array.from(set) before stringifying.
Test edge cases instantly — no setup needed