· 6 min read
Why JSON.stringify drops undefined, Map, Set and Date types
JSON.stringify silently drops undefined, empties Map and Set, and turns Dates into strings. Here is every rule and how to serialize safely.

JSON.stringify does not throw when it meets a value JSON cannot represent. It quietly drops undefined, functions and symbols from objects, turns NaN and Infinity into null, serializes a Map or Set as {}, and converts a Date into a string that stays a string when you parse it back. The only values that make it throw are BigInt and circular references. The fix is to know which values change shape and to convert them yourself with a replacer function, a toJSON method, or a different cloning tool.
This catches people because the output is valid JSON. Nothing errors, nothing logs, and the data that arrives on the other side is simply different from the data you sent.
Reproduce it
Take an object with a few ordinary JavaScript values in it:
const data = {
a: undefined,
f() {},
s: Symbol('x'),
n: NaN,
i: Infinity,
d: new Date(0),
m: new Map([[1, 2]]),
set: new Set([1]),
re: /x/g,
err: new Error('boom'),
};
console.log(JSON.stringify(data));
In Node 22 this prints:
{"n":null,"i":null,"d":"1970-01-01T00:00:00.000Z","m":{},"set":{},"re":{},"err":{}}
Three keys are gone, two numbers became null, the Map, Set, regular expression and error all became empty objects, and the error message is lost. Every one of those changes happened without a warning.
Why does JSON.stringify drop undefined?
JSON is a small format. It has strings, numbers, booleans, null, arrays and objects, and nothing else. JSON.stringify has to decide what to do with every JavaScript value that falls outside that list, and the specification picks a different answer depending on where the value sits.
Inside an object, a property whose value is undefined, a function or a symbol is skipped entirely. The key does not appear in the output. Inside an array, the same values become null, because removing them would shift every index after them:
JSON.stringify([undefined, () => {}, Symbol('x'), NaN]);
// '[null,null,null,null]'
At the top level there is nothing to write, so the call returns the JavaScript value undefined, not a string:
JSON.stringify(undefined); // undefined
JSON.stringify(() => {}); // undefined
That last case matters when you pass the result to something that expects a string, such as a request body or localStorage.setItem, which will store the text "undefined".
Numbers have their own rule. JSON has no way to write NaN, Infinity or -Infinity, so they become null. A division by zero somewhere upstream can quietly turn a metric into null in your API response.
What happens to Date, Map and Set?
Date has a built-in toJSON method. A toJSON method is a function on an object that JSON.stringify calls first, serializing whatever it returns instead of the object itself. For dates it returns an ISO 8601 string, so the date survives the trip as text. JSON.parse has no idea that text was ever a date, and gives you a string back:
const back = JSON.parse(JSON.stringify({ d: new Date(0) }));
typeof back.d; // 'string'
Code that later calls back.d.getTime() throws a TypeError. Converting it back with new Date(back.d) works, but watch how date-only strings are interpreted, which is covered in why new Date('2026-09-30') shows the wrong day.
Map, Set, RegExp and Error have no toJSON method and keep their data in internal slots rather than in own enumerable properties. JSON.stringify only looks at own enumerable string keys, finds none, and writes {}. An Error's message and stack are own properties, but they are not enumerable, so they are skipped too.
Which values make JSON.stringify throw?
Only two common cases raise an error instead of silently changing the data:
JSON.stringify({ big: 10n });
// TypeError: Do not know how to serialize a BigInt
const node = {};
node.self = node;
JSON.stringify(node);
// TypeError: Converting circular structure to JSON
BigInt throws because there is no safe default. Writing it as a number could lose precision in any parser that reads numbers as 64-bit floats, and writing it as a string would change its type. A circular reference, meaning an object that contains a path back to itself, would produce infinite output.
Here is the full picture:
| Value | In an object | In an array | At the top level |
|---|---|---|---|
undefined, function, symbol | Key removed | null | Returns undefined |
NaN, Infinity | null | null | "null" |
Date | ISO string | ISO string | ISO string |
Map, Set, RegExp, Error | {} | {} | "{}" |
BigInt | Throws | Throws | Throws |
| Circular reference | Throws | Throws | n/a |
How do you serialize these values safely?
Pick the fix based on whether the data has to leave the process as JSON.
Use a replacer for the types you care about
The second argument to JSON.stringify is a replacer function. It is called for every key and value, and whatever it returns is serialized instead:
function replacer(key, value) {
if (typeof value === 'bigint') return value.toString();
if (value instanceof Map) return Object.fromEntries(value);
if (value instanceof Set) return [...value];
if (value instanceof Error) return { name: value.name, message: value.message };
return value;
}
JSON.stringify({ big: 10n, m: new Map([['a', 1]]) }, replacer);
// '{"big":"10","m":{"a":1}}'
One detail trips people up here. By the time the replacer sees a Date, its toJSON has already run, so value is a string. If you need the original object, use a regular function and read this[key], since this is the object that holds the key. Arrow functions do not get their own this, so this only works with function.
Send null when you mean "clear this field"
A common bug is a partial update request built like { name: undefined } to clear a field. The key never reaches the server, so the server leaves the field unchanged. Send null explicitly when absence and "set to empty" mean different things:
JSON.stringify({ a: 1, b: undefined }, (k, v) => (v === undefined ? null : v));
// '{"a":1,"b":null}'
Use structuredClone for copies inside JavaScript
JSON.parse(JSON.stringify(obj)) is a long-standing way to deep-copy an object, and every rule above applies to it. If the copy stays inside JavaScript, structuredClone is the better tool. It keeps Date, Map, Set, RegExp, BigInt, undefined values and circular references intact, and it throws on functions rather than dropping them. It is available in all current browsers and in Node 17 and later.
Key takeaways
JSON.stringifysilently removesundefined, functions and symbols from objects and turns them intonullin arrays.NaNandInfinitybecomenull, andMap,Set,RegExpandErrorbecome{}.- Dates become ISO strings and stay strings after
JSON.parse, so convert them back on purpose. BigIntand circular references are the only common values that throw.- Use a replacer for wire formats and
structuredClonefor in-memory deep copies.
FAQ
Can JSON.parse turn date strings back into Date objects?
Yes, with a reviver, the second argument to JSON.parse. It is called for every key and value, so you can check for known date fields or an ISO pattern and return new Date(value). Matching by field name is safer than matching every string that looks like a date.
Why does JSON.stringify of an Error return an empty object?
An error's message and stack are own properties, but they are not enumerable, and JSON.stringify only serializes enumerable ones. Copy the fields you need into a plain object, either in a replacer or before logging.
Is a key with an undefined value the same as a missing key?
After JSON.stringify, yes, because the key is removed. In JavaScript they differ: 'b' in obj is true for { b: undefined } and false for {}. If that difference carries meaning in your API, send null instead.