JSON.parse and JSON.stringify.
Read and write JSON.
The two methods every JavaScript project uses. JSON.parse converts a JSON string into a JavaScript value. JSON.stringify converts a JavaScript value into a JSON string. They're symmetric in name, asymmetric in behavior — Dates survive stringify but not parse; undefined and functions are silently dropped; circular references throw. This page covers the syntax, the edge cases, and the patterns that don't lose data.
Short answer: JSON.parse(str) reads a JSON string into a JS value. JSON.stringify(obj) writes a JS value to a JSON string. Wrap parse in try/catch — bad data throws. Use the replacer/reviver arguments to handle Dates, undefined, and custom types. JSON.stringify throws on circular references — remove them or use a library. Both methods are deep — they traverse the whole structure.
✅ The two directions
Parse reads. Stringify writes.
// Parse — JSON string → JS value
const json = '{"name":"Alice","age":30,"active":true}';
const obj = JSON.parse(json);
console.log(obj);
// { name: 'Alice', age: 30, active: true }
// Stringify — JS value → JSON string
const data = { name: 'Alice', age: 30, active: true };
const str = JSON.stringify(data);
console.log(str);
// '{"name":"Alice","age":30,"active":true}'
// Pretty-printed
const pretty = JSON.stringify(data, null, 2);
console.log(pretty);
// {
// "name": "Alice",
// "age": 30,
// "active": true
// }
// Round-trip — stringify then parse
const back = JSON.parse(JSON.stringify(data));
console.log(back.name); // 'Alice'
What the round-trip preserves:
✅ Objects, arrays, strings, numbers, booleans, null
✅ Nested structures
✅ Object property order (in modern browsers)
What the round-trip does NOT preserve:
❌ undefined (dropped from objects, becomes null in arrays)
❌ functions (dropped entirely)
❌ Symbol keys (dropped)
❌ BigInt (throws)
❌ Dates (converted to ISO strings)
❌ Map, Set (become {})
❌ RegExp (becomes {})
❌ Class instances (lose their prototype)
❌ Circular references (throw)
❌ NaN, Infinity (become null)
The most important asymmetry:
const obj = { d: new Date() };
const str = JSON.stringify(obj);
// '{"d":"2026-11-07T12:00:00.000Z"}'
const back = JSON.parse(str);
// { d: '2026-11-07T12:00:00.000Z' } ← string, not Date
Date → string via stringify.
string does not become Date via parse.
The asymmetry is by design — JSON has no Date.
For lossless serialization, use a library:
superjson, devalue, flatted — all preserve
Dates, Maps, Sets, and other types.
✅ Error handling for parse
Bad data throws. Always wrap.
// ❌ Bad data throws
JSON.parse('not json');
// SyntaxError: Unexpected token 'o'
JSON.parse('');
// SyntaxError: Unexpected end of JSON input
JSON.parse('404');
// SyntaxError: Unexpected token '<'
// ✅ Wrap in try/catch
function safeParse(str, fallback = null) {
try {
return JSON.parse(str);
} catch (e) {
console.error('Invalid JSON:', e.message);
return fallback;
}
}
console.log(safeParse('{"a":1}')); // { a: 1 }
console.log(safeParse('invalid')); // null
console.log(safeParse('', [])); // []
// ✅ Common pattern for fetch
async function fetchData(url) {
try {
const res = await fetch(url);
const text = await res.text();
return safeParse(text, null);
} catch (e) {
return null;
}
}
Why JSON.parse throws:
JSON is a strict format.
→ Double quotes for strings: {"key":"value"}
→ No trailing commas: {"a":1,"b":2} not {"a":1,"b":2,}
→ No comments
→ No single quotes
→ No unquoted keys
→ No functions, undefined, Date, Map
If the input is any of these, JSON.parse throws:
→ JavaScript object literal (single quotes)
→ HTML (a 404 page served with 200 status)
→ Empty string
→ Already-parsed object (parse expects a string)
→ Truncated response (network dropped mid-stream)
The most common production bug:
❌ const res = await fetch(url);
const data = await res.json(); // throws on 404
✅ const res = await fetch(url);
if (!res.ok) throw new Error(res.status);
const data = await res.json();
Always check res.ok before .json().
A 404 returns HTML, and .json() throws on HTML.
For localStorage:
❌ const data = JSON.parse(localStorage.getItem('key'));
// throws if item is null
✅ const data = safeParse(localStorage.getItem('key'));
// or check for null first
localStorage.getItem returns null for missing keys.
JSON.parse(null) throws.
Rule: wrap JSON.parse in try/catch,
or know that the source is always valid.
The second is rare — data from the network,
from localStorage, from user input —
is rarely guaranteed.
✅ Replacer and reviver
Custom handling for special types.
const data = {
name: 'Alice',
createdAt: new Date('2026-01-15'),
score: undefined,
meta: { tags: ['a', 'b'] }
};
// Replacer — custom logic during stringify
const str = JSON.stringify(data, (key, value) => {
// Convert Dates to a marker object
if (value instanceof Date) {
return { __type: 'Date', value: value.toISOString() };
}
return value;
});
console.log(str);
// {"name":"Alice","createdAt":{"__type":"Date","value":"..."},"meta":{"tags":["a","b"]}}
// Reviver — custom logic during parse
const parsed = JSON.parse(str, (key, value) => {
if (value && typeof value === 'object' && value.__type === 'Date') {
return new Date(value.value);
}
return value;
});
console.log(parsed.createdAt);
// Date object, not string
console.log(parsed.createdAt instanceof Date);
// true
The replacer signature: JSON.stringify(value, (key, val) => transformedVal) JSON.stringify(value, (key, val) => transformedVal, indent) Called for every key-value pair in the object. The `key` is the property name, or '' for the root. Return the transformed value, or undefined to omit. Common uses: ✅ Convert Dates to a custom format ✅ Omit sensitive fields (passwords, tokens) ✅ Convert Sets and Maps to arrays ✅ Convert BigInt to strings ✅ Redact circular references The reviver signature: JSON.parse(str, (key, val) => transformedVal) Called for every parsed key-value pair. Return the transformed value, or undefined to omit. Common uses: ✅ Convert date strings back to Date objects ✅ Restore Sets and Maps ✅ Hydrate class instances ✅ Validate the parsed shape The two together give you lossless serialization: stringify(obj, replacer) → parse(str, reviver) For complex types (Map, Set, custom classes), use a library: superjson, devalue, or flatted. They handle the edge cases (nested types, circular references, type collisions) that custom replacers often miss.
Don't use JSON.parse(JSON.stringify(obj)) as a deep clone. This is a common pattern that looks like a deep clone but has real gaps. It loses Dates (becomes strings), undefined (dropped), functions (dropped), Map/Set (become {}), RegExp (becomes {}), class instances (lose their prototype), and it throws on circular references. It also fails on BigInt. For deep clones, use structuredClone(obj) (supported in all modern browsers and Node 17+). It's faster, handles more types, and preserves what JSON loses. The JSON.parse(JSON.stringify()) trick was necessary before structuredClone existed — in 2026, it's not.
⚠️ Common mistakes
The JSON mistakes that cost real time.
Bad data throws. Fix: wrap JSON.parse in try/catch, or ensure the source is valid.
.json() before checking res.ok404 returns HTML, not JSON. Fix: check res.ok first.
Date becomes string. Fix: reviver, or use structuredClone.
Dropped from objects, null in arrays. Fix: transform with replacer if needed.
Throws TypeError. Fix: remove the cycle, or use flatted.
JSON.parse(JSON.stringify(obj)) for deep cloneLoses Date, Map, Set, undefined, BigInt. Fix: structuredClone(obj).
localStorage.getItem without a null checkJSON.parse(null) throws. Fix: check for null before parsing.
Throws TypeError. Fix: convert to string first, or use a replacer.
❓ Frequently asked
What is the difference between JSON.parse and JSON.stringify?
They go in opposite directions. JSON.parse converts a JSON string into a JavaScript value. JSON.stringify converts a JavaScript value into a JSON string. Use parse when you receive data as text. Use stringify when you need to send data as text.
Why does JSON.parse throw on valid-looking data?
Three common causes. (1) The data isn't JSON — it's HTML, a JS object literal, or empty. (2) The data has a trailing comma or unquoted key — that's JavaScript, not JSON. (3) The data is already parsed. Fix: check the source, log the raw text, and use try/catch around JSON.parse in production.
Why does JSON.stringify drop undefined and functions?
JSON doesn't have undefined or functions — they're not valid JSON values. When stringifying an object, undefined properties and function properties are silently dropped. When stringifying an array, they become null. This is by spec. If you need to keep these values, transform them first with a replacer.
How do I stringify a Date?
JSON.stringify converts a Date to its ISO 8601 string automatically. But JSON.parse does not convert it back to a Date — you get a string. To preserve Dates, use a reviver. Or use a library like flatted or superjson. The asymmetry is by design — JSON has no Date type.
How do I stringify an object with circular references?
JSON.stringify throws on circular references. Fixes: (1) Remove the circular reference. (2) Use a replacer that tracks seen objects. (3) Use a library like flatted. The most common case is a data structure you didn't expect to be circular — check your data model.
Is JSON.parse faster than JSON.stringify?
Generally yes — parsing is usually 2-3x faster than stringifying for the same data. But both are fast enough for typical data. The performance matters only at scale. Optimize after measuring, not before.
🔗 Continue learning
Next in this series: