Working With JSON in JavaScript: parse(), stringify(), and Beyond
JSON and JavaScript share a naming history — JSON literally stands for JavaScript Object Notation — but the relationship between the two goes deeper than syntax similarity. JavaScript has built-in, native JSON handling through the global JSON object, and it comes with more depth than most guides cover: custom serialization, selective filtering, error recovery, and behaviors around edge cases like undefined, circular references, and Date objects that catch even experienced developers off guard.
This guide covers JSON.parse() and JSON.stringify() from the basics through to their less commonly known options — the reviver and replacer parameters — plus the specific gotchas worth knowing before they cause a bug in production.
If you’re newer to JSON’s underlying rules, our JSON syntax guide and Python JSON guide are useful companion reading, since the syntax itself is identical across every language — only the surrounding code differs.
JSON.parse(): String to JavaScript Value
JSON.parse() converts a JSON string into a native JavaScript value — an object, array, string, number, boolean, or null:
const data = JSON.parse('{"name": "Kings Tools", "free": true, "version": 2}');
console.log(data);
// { name: 'Kings Tools', free: true, version: 2 }
Catching Parse Errors
When the input isn’t valid JSON, JSON.parse() throws a SyntaxError rather than returning null or undefined — which means you need a try/catch block around any parse call where the input isn’t guaranteed to be valid:
try {
JSON.parse('{"name": "Kings Tools", "version": 2,}');
} catch (e) {
console.log(e.name); // SyntaxError
console.log(e.message); // Expected double-quoted property name in JSON at position 37 (line 1 column 38)
}
Modern JavaScript engines (V8, used in Chrome and Node.js) include the line and column directly in the error message, which is the same underlying detail our JSON Formatter and Validator surfaces visually rather than as raw error text.

A critical, often-missed point: if you’re calling JSON.parse() on data from an external source — an API response, user input, localStorage — and you skip the try/catch, one malformed response can crash your entire script. This is one of the most common uncaught-exception bugs in production JavaScript.
JSON.stringify(): JavaScript Value to String
Going the other direction, JSON.stringify() converts a JavaScript value into a JSON string:
const obj = { tool: 'formatter', modes: ['beautify', 'minify'], free: true };
console.log(JSON.stringify(obj));
// {"tool":"formatter","modes":["beautify","minify"],"free":true}
console.log(JSON.stringify(obj, null, 2));
// {
// "tool": "formatter",
// "modes": [
// "beautify",
// "minify"
// ],
// "free": true
// }
That third argument — the indent parameter — controls pretty-printing. Pass a number for that many spaces of indentation, or omit it (or pass null) for compact, minified output.
What Gets Silently Dropped or Changed
This is where JSON.stringify() behavior surprises people who assume it’s a straightforward, lossless conversion. It isn’t, and knowing these behaviors up front avoids some genuinely confusing bugs.
undefined values are removed from objects, but become null inside arrays:
JSON.stringify({ a: 1, b: undefined, c: null });
// {"a":1,"c":null} <- "b" is gone entirely
JSON.stringify([1, undefined, 3]);
// [1,null,3] <- undefined became null, not removed
This inconsistency is a real source of bugs — code that checks Object.keys(parsed).length after a round trip may get a different count than expected if any values were undefined.
Functions are silently omitted:
JSON.stringify({ a: 1, fn: function() {} });
// {"a":1}
No error, no warning — the function key just doesn’t appear in the output, since JSON has no way to represent executable code.
Date objects become ISO strings, not dates:
JSON.stringify({ created: new Date('2026-01-01T00:00:00Z') });
// {"created":"2026-01-01T00:00:00.000Z"}
This is convenient for storage and transmission, but means that after JSON.parse(), you’ll get back a plain string, not a Date object — you have to explicitly convert it back yourself (more on this below with the reviver parameter).
NaN and Infinity both become null:
JSON.stringify({ a: NaN, b: Infinity });
// {"a":null,"b":null}
Since standard JSON has no way to represent these values, JavaScript substitutes null rather than throwing an error — which can silently corrupt numeric data if you’re not aware of it.
Circular references throw an error, not a warning:
const circ = {};
circ.self = circ;
JSON.stringify(circ);
// TypeError: Converting circular structure to JSON
Unlike the silent behaviors above, this one fails loudly — which is actually the safer behavior, but worth knowing about before it happens in production and looks like a mysterious crash.
The replacer Parameter: Filtering What Gets Serialized
JSON.stringify() accepts a second argument — the replacer — that lets you control exactly what gets included. It can be either a function or an array of allowed keys.
As a function, useful for removing sensitive fields like passwords:
const user = { name: 'Alex', password: 'secret123', age: 29 };
JSON.stringify(user, (key, value) => key === 'password' ? undefined : value);
// {"name":"Alex","age":29}
As an array, useful as an explicit whitelist:
JSON.stringify(user, ['name', 'age']);
// {"name":"Alex","age":29}
Both approaches produce the same result here, but the function form is more flexible for conditional logic, while the array form is simpler and self-documenting for a fixed set of fields.
The reviver Parameter: Transforming Data on the Way In
JSON.parse() has a lesser-known second argument too — a reviver function that runs on every key-value pair as the JSON is parsed, letting you transform values on the way in. This is exactly how you solve the Date problem mentioned above:
const parsed = JSON.parse(
'{"name":"Alex","createdAt":"2026-01-01T00:00:00.000Z"}',
(key, value) => key === 'createdAt' ? new Date(value) : value
);
console.log(parsed.createdAt instanceof Date); // true
Without the reviver, createdAt would just be a plain string after parsing — the reviver is what converts it back into an actual Date object your code can call .getMonth() or similar methods on.
Custom Serialization With toJSON()
If an object needs custom serialization behavior every time it’s stringified, define a toJSON() method on it — JSON.stringify() calls this method automatically if it exists, using whatever it returns instead of the object’s own properties:
class Point {
constructor(x, y) { this.x = x; this.y = y; }
toJSON() { return `(${this.x}, ${this.y})`; }
}
JSON.stringify({ point: new Point(3, 4) });
// {"point":"(3, 4)"}
This is genuinely useful for classes that have a natural string or simplified representation, rather than exposing their full internal structure.
A Common (and Flawed) Trick: Deep Cloning
A well-known pattern for deep-cloning a plain object is round-tripping it through JSON.stringify() and JSON.parse():
const original = { a: 1, nested: { b: 2 } };
const clone = JSON.parse(JSON.stringify(original));
clone.nested.b = 999;
console.log(original.nested.b); // 2 — unaffected
console.log(clone.nested.b); // 999
It works, and it’s simple — but it inherits every limitation covered above. Functions get dropped, undefined gets dropped or converted, Date objects become strings instead of staying Date objects, and circular references throw an error instead of cloning. For simple, JSON-safe data, it’s a fine shortcut. For anything with functions, dates, or circular structures, structuredClone() (a newer built-in JavaScript function) is a more reliable choice.
Frequently Asked Questions
Does JSON.parse() work the same in Node.js and the browser? Yes — both run on JavaScript engines that implement the same JSON global object per the ECMAScript specification, so behavior is identical.
How do I safely parse JSON from localStorage that might not exist yet? Check for null before parsing, since localStorage.getItem() returns null for a missing key, and JSON.parse(null) actually succeeds and returns the value null rather than throwing — which can mask a missing-key bug if you’re not careful:
const raw = localStorage.getItem('settings');
const settings = raw ? JSON.parse(raw) : {};
What does response.json() do in the Fetch API? It’s a convenience method that reads the response body and runs JSON.parse() on it for you, returning a Promise that resolves to the parsed value — equivalent to manually calling .text() followed by JSON.parse(), just combined into one step.
Summary
JSON.parse() and JSON.stringify() cover the basics quickly, but the real depth — reviver, replacer, toJSON(), and the specific ways undefined, functions, dates, and circular references behave — is where most JavaScript JSON bugs actually come from. Knowing these behaviors ahead of time, rather than discovering them from a production bug, is the difference between JSON handling that “just works” and code that silently loses or corrupts data at the edges.
