UnblockDevs

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

1

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.

javascriptHow undefined is handled in objects, arrays, and at the top level
// 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 arrays

Silent drops cause PATCH vs PUT semantic bugs

When you send a PATCH request with 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!)

❌ Bad
// 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

✅ Good
// 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));
2

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.

javascriptnull behavior vs undefined
// 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)); // null
3

Date 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.

javascriptDate serialization and round-trip
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; // true

Date.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.

4

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.

javascriptNaN and Infinity become null
// 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 corrupted

NaN from division or failed parse silently becomes null in JSON

NaN silently becomes null — server receives wrong type

❌ Bad
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 error

Validate numbers before serialize — no silent corruption

✅ Good
// 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;
});
5

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.

javascriptCircular reference — when and why it throws
// 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
javascriptFix circular references — three approaches
// 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 start
6

BigInt — 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.

javascriptBigInt — fix strategies
// 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"}'
7

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.

javascriptMap and Set lose their data silently
// 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);
8

Symbol — Always Omitted

javascriptSymbol keys and values are 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 serialize
9

Quick 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

Paste any JavaScript object with unusual values (undefined, BigInt, circular refs via toJSON override) into the JSON.stringify() online tool to see exactly how they serialize. Faster than opening a browser console.

Frequently Asked Questions