JSON is the format almost every API speaks, and JavaScript gives you exactly two functions for it: JSON.stringify to write it and JSON.parse to read it. The gap between those two calls and real-world data is where most of the trouble lives.
What is JSON, and what is it not?
JSON stands for JavaScript Object Notation. It is a text format, not a JavaScript value, and its rules are strict: double quotes around keys and strings, no trailing commas, no comments, no single quotes, no undefined, no functions. That strictness matters because the data you receive over the network arrives as text, and turning that text into an object is a step that can fail.
const payload = JSON.stringify({ name: 'Ada', tags: ['js'] });
console.log(payload); // {"name":"Ada","tags":["js"]}
const back = JSON.parse(payload);
console.log(back.name); // Ada
console.log(back.tags[0]); // js
console.log(typeof back); // object
Two different things are happening in those five lines. stringify turns a live JavaScript value into a string of characters. parse takes that string and builds a brand-new object from it — a different object from the one you started with, which is why the result no longer shares references with the original.
How do I turn an object into JSON text?
Most of the time JSON.stringify(value) is all you need. Two optional arguments change the output: a replacer, and the number of spaces to indent with.
console.log(JSON.stringify({ id: 7, note: undefined, fn: () => {} }));
// {"id":7}
console.log(JSON.stringify({ when: new Date('2026-03-01') }));
// {"when":"2026-03-01T00:00:00.000Z"}
console.log(JSON.stringify({ a: 1 }, null, 2));
// {
// "a": 1
// }
Three behaviours are worth noticing. Properties whose value is undefined or a function are dropped entirely — they do not survive as null. A Date becomes an ISO 8601 string, because JSON has no date type. And the third argument, an indentation count, is what makes the output readable in a log file or a configuration file.
What happens when the JSON text is invalid?
JSON.parse throws a SyntaxError when the text does not follow the rules. The message carries a position, which is genuinely useful when the text came from a file.
try {
JSON.parse('{ "name": "Ada" ');
} catch (error) {
console.log(error instanceof SyntaxError); // true
console.log(error.message);
}
// Expected ',' or '}' after property value in JSON at position 16 (line 1 column 17)
The exact wording of the message is engine-specific, so never match on its text in your code. What you can rely on is that it is a SyntaxError and that it is thrown rather than returned.
How do I handle a parse error without crashing?
Wrap the call. A small helper that returns a result object keeps the error handling in one place instead of scattering try and catch around every call site.
function parseJson(text) {
try {
return { ok: true, value: JSON.parse(text) };
} catch {
return { ok: false, value: null };
}
}
console.log(parseJson('{"a":1}')); // { ok: true, value: { a: 1 } }
console.log(parseJson('nope')); // { ok: false, value: null }
Two styles are common here. The helper above returns a result object, which forces the caller to check ok before touching the value. The alternative is to let the error travel up to a single top-level handler. Either approach is fine; what matters is that malformed input from outside your program never takes the program down.
How do I convert values back on the way in?
Both functions accept an optional function that gets a say in every value. The one for parse is called a reviver, and it can transform each value as it is read. The one for stringify is called a replacer, and it can filter or transform values on the way out.
const json = '{"id":1,"createdAt":"2026-01-05T10:00:00.000Z"}';
const order = JSON.parse(json, (key, value) => (
key === 'createdAt' ? new Date(value) : value
));
console.log(order.createdAt instanceof Date); // true
const text = JSON.stringify({ id: 1, secret: 'x' }, ['id']);
console.log(text); // {"id":1}
The reviver turned the date string back into a Date object. The replacer was given an array of key names, which is the whitelist form: only those keys are written out. The same replacer can also be a function, in which case it receives the key and the value and returns whatever should be written instead — useful for stripping fields before you send data anywhere.
What does JSON leave out that you might want?
A short list, because these are the things that surprise people. Values of undefined, functions and symbols are dropped. Dates become strings. Maps and Sets become empty objects, because they have no enumerable own properties. Infinity and NaN become null. A circular reference makes stringify throw a TypeError.
const circular = { name: 'loop' };
circular.self = circular;
try {
JSON.stringify(circular);
} catch (error) {
console.log(error.constructor.name + ': ' + error.message);
}
// TypeError: Converting circular structure to JSON
The message goes on to show the path where the cycle closes, which is a useful clue when the object is large and you cannot see the loop by eye.
When is JSON the right tool?
Three situations cover most real uses. Saving data in localStorage, because storage only holds strings. Sending a payload with fetch, where the body of a POST request is normally JSON text. And reading a configuration file from disk in Node. In all three the pattern is identical: stringify on the way out, parse on the way in, and treat the parse as a step that can fail.
Try it yourself: create a file called json.js with const order = { id: 'A-1', total: 42, placedAt: new Date('2026-02-01'), notes: undefined, paid: false };. Print JSON.stringify(order) and notice both the date and the missing notes. Parse that string back with a reviver that turns placedAt into a Date again, and print order.placedAt instanceof Date. Then wrap JSON.parse in a try and catch and attempt to parse 'not json' to see the error for yourself. Finally, print a trimmed version of the order using JSON.stringify with an indentation of 2 and a replacer array of ['id', 'total'].