JavaScript JSON: JSON.stringify, JSON.parse, try/catch and Deep Copies
We learn JSON's syntax rules, indentation and replacers with JSON.stringify, which values get lost, JSON.parse and revivers, catching broken JSON with try/catch, and why structuredClone wins for deep copies.
Version note: The code in this post targets ES2026 and current browsers (Chrome, Edge, Firefox, Safari).
Predict First
In the previous post we learned about objects. Now we'll see how to turn those objects into text. Look at this object and the one-line conversion:
const data = {
name: "Ada",
age: undefined,
greet: function () { return "Hello"; },
joined: new Date("2026-01-15T10:00:00Z"),
score: NaN
};
console.log(JSON.stringify(data));
There are five properties. How many of them end up in the text printed to the console, and in what form? Make your guess before you keep reading. The answer is in the section where we talk about what JSON can't carry.
What Is JSON?
JSON stands for JavaScript Object Notation. Despite the JavaScript in its name, it's actually a language-independent, plain-text data format. When a server sends you a product list, when an app writes its settings to a file, or when the browser stores some data, JSON is very likely involved. Python, Java, Go, PHP, any language you can think of can read and write JSON.
Think of an object as the furniture in a house. When you move, you can't just throw the furniture into the truck as it is; you pack it and stick labels on the boxes. JSON is those boxes. An object is a living structure in memory; JSON is its portable version, put down on paper. The other side opens the boxes and arranges the furniture in its own house. In JavaScript, this two-way job comes down to two methods: JSON.stringify, which turns an object into text, and JSON.parse, which turns text into an object.
JSON Syntax Rules
JSON looks a lot like an object literal, but it's much stricter. Here's what a piece of JSON text looks like; paste this into your browser's console and try it:
const text = `{
"name": "Ada",
"age": 28,
"isAdmin": false,
"skills": ["HTML", "CSS"],
"address": { "city": "Izmir" },
"phone": null
}`;
const user = JSON.parse(text);
console.log(user.address.city); // "Izmir"
Let's go over the rules quickly. Keys must always be in double quotes. Strings are written only with double quotes too; single quotes and backticks are invalid. There's no comma after the last element. You can't write comments. Only six kinds of values are allowed: strings, numbers, true/false, null, objects and arrays. Things like undefined, functions, NaN, Infinity or Date have no equivalent in JSON.
This strictness isn't a shortcoming; it's a choice. The fewer and clearer the rules, the more certain it is that different languages read the same text the same way.
JSON.stringify: From Object to Text
const product = { title: "Keyboard", price: 750, tags: ["usb", "mechanical"] };
const json = JSON.stringify(product);
console.log(json); // {"title":"Keyboard","price":750,"tags":["usb","mechanical"]}
console.log(typeof json); // "string"
The result is no longer an object but a string. The keys are wrapped in double quotes and the spaces are gone. In this form it's ready to be sent to a server or stored somewhere.
Indentation for readable output
Single-line output is ideal for machines but tiring for human eyes. The third argument of JSON.stringify is the amount of indentation:
const product = { title: "Keyboard", price: 750 };
console.log(JSON.stringify(product, null, 2));
// {
// "title": "Keyboard",
// "price": 750
// }
It's very handy while debugging or when writing a settings file. When sending data over the network, though, leave out the indentation, or you'll be shipping useless whitespace.
Picking and hiding with a replacer
The second argument, which we just passed as null, is the replacer. Give it an array and only those keys make it into the text. Give it a function and it's called for every key-value pair, and whatever you return is used; return undefined and that property is skipped:
const account = { name: "Ada", email: "ada@example.com", password: "secret123" };
console.log(JSON.stringify(account, ["name", "email"]));
// {"name":"Ada","email":"ada@example.com"}
const safe = JSON.stringify(account, (key, value) => {
if (key === "password") {
return undefined;
}
return value;
});
console.log(safe); // {"name":"Ada","email":"ada@example.com"}
It's a practical way to leave sensitive fields out when you write an object to a log or send it somewhere.
What Silently Disappears?
Here's the answer to the opening question:
// {"name":"Ada","joined":"2026-01-15T10:00:00.000Z","score":null}
Three of the five properties remain, and even they aren't exactly the same. Values that have no JSON equivalent don't throw errors; they either quietly disappear or turn into something else. Properties whose value is undefined or a function are skipped entirely. NaN and Infinity become null. A Date is converted to a string in ISO format; when you read it back, it's no longer a date but plain text. Inside an array, the same situation plays out a little differently:
console.log(JSON.stringify([1, undefined, function () {}, NaN]));
// [1,null,null,null]
console.log(JSON.stringify({ tags: new Set(["a", "b"]) })); // {"tags":{}}
try {
JSON.stringify({ id: 10n });
} catch (error) {
console.log(error.name); // "TypeError"
}
Skipping an element in an array would break the order, so the engine puts null in its place. Map and Set turn into an empty object and their contents are lost. The bigint we saw earlier gets no mercy at all and throws a TypeError. In short, put only plain data into JSON; if there are functions, dates or special types inside, think about what will happen before you convert.
JSON.parse: From Text to Object
The opposite direction is much simpler. You pass valid JSON text and get an object or an array in return:
const text = '{"title":"Keyboard","price":750,"inStock":true}';
const product = JSON.parse(text);
console.log(product.title); // "Keyboard"
console.log(product.price + 50); // 800
console.log(JSON.parse("[1, 2, 3]").length); // 3
Transforming values with a reviver
The second argument of JSON.parse is the reviver. It's called for every key-value pair and lets you change the value. Its most common use is turning dates that arrived as strings back into Date objects:
const text = '{"title":"Meeting","date":"2026-01-15T10:00:00.000Z"}';
const meeting = JSON.parse(text, (key, value) => {
if (key === "date") {
return new Date(value);
}
return value;
});
console.log(meeting.date instanceof Date); // true
console.log(meeting.date.getUTCFullYear()); // 2026
ES2026: the source text passed to the reviver
With ES2026 the reviver gets a third argument: a context object that carries the value's original form in the JSON text. The safe integer limit we saw in the post on numbers explains why this is useful:
const text = '{"id": 12345678901234567890}';
console.log(JSON.parse(text).id); // 12345678901234567000
const order = JSON.parse(text, (key, value, context) => {
if (key === "id") {
return BigInt(context.source);
}
return value;
});
console.log(order.id); // 12345678901234567890n
In a normal parse, the number is already rounded before it even reaches the reviver. context.source, on the other hand, gives you the digits exactly as they appear in the text, so we can turn them into a bigint without losing anything. This feature is available in all current browsers.
Catching Broken JSON with try/catch
There's no guarantee that text coming from outside is valid JSON. If you pass broken text to JSON.parse, it throws a SyntaxError, and unless you've taken precautions, your code stops right there:
const broken = "{'name': 'Ada'}";
try {
const user = JSON.parse(broken);
console.log(user.name);
} catch (error) {
console.log(error.name); // "SyntaxError"
console.log("Couldn't read the data, using default settings.");
}
console.log("The code keeps running.");
The try block says "try this." If an error is thrown inside it, the engine skips the rest of the block and jumps straight to catch; the error variable carries the error that was thrown. Because the error is caught, the program doesn't crash and the last line runs too. error.message gives you the details, but its text varies from browser to browser, so write your own message instead of showing it to the user directly. We'll go into error handling, finally and writing your own error types in the Intermediate series.
Deep Copies: JSON or structuredClone?
You'll often run into this line in older code:
const original = {
name: "Ada",
birthday: new Date("1998-05-20T00:00:00Z"),
nickname: undefined,
address: { city: "Izmir" }
};
const viaJson = JSON.parse(JSON.stringify(original));
const viaClone = structuredClone(original);
console.log(typeof viaJson.birthday); // "string"
console.log(viaClone.birthday instanceof Date); // true
console.log("nickname" in viaJson); // false
console.log("nickname" in viaClone); // true
Turning an object into text and reading it straight back produces a fresh copy of everything nested inside. Before structuredClone, this was the shortest way to get a deep copy. But all the losses you just saw apply here too: dates turn into strings, undefined values vanish, Map and Set come back empty, and an object that references itself gives you a TypeError. The structuredClone we saw in the previous post copies all of these types correctly and is both clearer and more reliable. Use it for deep copies, and keep JSON for moving data around.
Common Pitfalls
Parsing something that's already an object
const user = { name: "Ada" };
try {
JSON.parse(user);
} catch (error) {
console.log(error.name); // "SyntaxError"
}
JSON.parse expects a string. If you give it an object, it first converts the object to a string, which becomes "[object Object]", and that's not valid JSON. Know whether the value in your hands is text or an object; don't parse data that has already been parsed.
Stringifying twice
const once = JSON.stringify({ a: 1 });
const twice = JSON.stringify(once);
console.log(once); // {"a":1}
console.log(twice); // "{\"a\":1}"
If you stringify JSON that's already a string, you get a string full of escaped quotes. When the other side opens it, it finds another string, not an object.
Mistaking an object literal for JSON
The { name: "Ada" } in your code is an object literal, not JSON. JSON is always text. When you write JSON by hand, habits from JavaScript like single quotes, unquoted keys or trailing commas leak in, and JSON.parse throws an error. If you're not sure, run the text through JSON.parse once in the console to check.
A Small Experiment
★☆☆ Print your own me object with JSON.stringify, first on a single line, then with 2-space indentation. Convert the output back with JSON.parse and read one of its properties to confirm it came back correctly.
★★☆ Predict before running: what does JSON.stringify({ a: [undefined, 2], b: undefined, c: Infinity, d: "null" }) print? Then run it and explain why each property ends up the way it does.
★★☆ Write a safeParse(text) function: if the text is valid JSON it should return the object, otherwise null. Try it with '{"ok": true}', "{ok: true}" and an empty string.
★★★ Write a replacer that converts a list of users to JSON while removing each user's password field and masking the email field in the "a***@example.com" format. The slice and indexOf we saw earlier in the post on strings will come in handy.
Strings, numbers, arrays, functions, objects and JSON are now in your hands. So far we've done everything in the console; now it's time to touch the page itself. In the next post we step into the DOM and see how to find, change and add elements on an HTML page with JavaScript.