Parsing JSON in JavaScript is something you'll do hundreds of times as a web developer. Fortunately, JavaScript has built-in support for it — no libraries needed. But there's more nuance to JSON.parse() and JSON.stringify() than the docs let on. Let's go through it properly.

JSON.parse() — Turning a String into an Object

JSON.parse() takes a JSON-formatted string and converts it into a JavaScript value. That value is usually an object or array, but it can also be a string, number, boolean, or null:

js
const jsonString = '{"name": "Alice", "age": 30, "active": true}';
const user = JSON.parse(jsonString);

console.log(user.name);    // "Alice"
console.log(user.age);     // 30
console.log(user.active);  // true
console.log(typeof user);  // "object"

Notice the input must be a string. A common mistake is trying to parse a value that's already an object — JSON.parse({}) throws a SyntaxError because it converts the object to "[object Object]" first, which isn't valid JSON.

Always Wrap JSON.parse() in a try/catch

Here's the thing nobody tells you: JSON.parse() throws a SyntaxError if the input is invalid JSON. If you're parsing data from an API, user input, or a file, you should always handle that error. A single malformed response can crash your app if you don't:

js
function safeParseJSON(str) {
  try {
    return { data: JSON.parse(str), error: null };
  } catch (err) {
    return { data: null, error: err.message };
  }
}

const { data, error } = safeParseJSON('{"name": "Alice"}');
if (error) {
  console.error('Invalid JSON:', error);
} else {
  console.log(data.name); // "Alice"
}

// Handles bad input gracefully
const result = safeParseJSON('not json at all');
console.log(result.error); // "Unexpected token 'o', "not json "... is not valid JSON"

I use a wrapper like this in almost every project. It makes error handling consistent and prevents ugly uncaught exceptions from bubbling up to your UI.

JSON.stringify() — Turning an Object into a String

JSON.stringify() does the reverse: it converts a JavaScript value into a JSON string. You'll use this when sending data to an API, saving to localStorage, or writing to a file:

js
const user = {
  name: "Bob",
  age: 25,
  roles: ["admin", "editor"],
  password: "secret123" // we'll handle this later
};

// Basic usage
const jsonString = JSON.stringify(user);
// '{"name":"Bob","age":25,"roles":["admin","editor"],"password":"secret123"}'

// Pretty-printed (great for logs and file output)
const prettyJson = JSON.stringify(user, null, 2);
console.log(prettyJson);
// {
//   "name": "Bob",
//   "age": 25,
//   "roles": ["admin", "editor"],
//   "password": "secret123"
// }

The third argument to JSON.stringify() is the indent level. Using 2 or 4 gives you readable output. The default (no third arg) gives you compact, minified JSON — better for network transmission.

What stringify() Drops Silently

This one bites developers regularly. JSON.stringify() silently omits certain values because JSON doesn't support them:

js
const data = {
  name: "Alice",
  greet: function() { return "hi"; },   // Functions → dropped
  undef: undefined,                      // undefined → dropped
  sym: Symbol("key"),                    // Symbols → dropped
  nan: NaN,                              // NaN → null
  inf: Infinity,                         // Infinity → null
  date: new Date("2024-01-15")           // Dates → ISO string
};

console.log(JSON.stringify(data, null, 2));
// {
//   "name": "Alice",
//   "nan": null,
//   "inf": null,
//   "date": "2024-01-15T00:00:00.000Z"
// }
// greet, undef, sym are GONE
Watch out: If you stringify an object with undefined values and then parse it, those keys will be missing entirely. This can cause subtle bugs if your code checks for obj.key === undefined later.

Fetching JSON from an API — The Real-World Pattern

In practice, most JSON parsing happens when you fetch data from an API. The Fetch API makes this straightforward — response.json() handles the parsing for you:

js
async function getUser(userId) {
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);

    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }

    const user = await response.json(); // parses JSON automatically
    return user;
  } catch (err) {
    console.error('Failed to fetch user:', err);
    return null;
  }
}

const user = await getUser(42);
if (user) {
  console.log(`Hello, ${user.name}!`);
}

response.json() is essentially JSON.parse(await response.text()). It also throws if the response body isn't valid JSON, so it's worth catching that separately in production code.

Working with Nested JSON Data

Deeply nested JSON is common in real APIs. Here's how you navigate it safely without crashing on missing keys:

js
const apiResponse = {
  "user": {
    "profile": {
      "address": {
        "city": "Berlin"
      }
    }
  }
};

// Unsafe — throws if any level is undefined
const city1 = apiResponse.user.profile.address.city; // "Berlin"

// Safe with optional chaining (ES2020+) — returns undefined instead of throwing
const city2 = apiResponse?.user?.profile?.address?.city; // "Berlin"
const zip   = apiResponse?.user?.profile?.address?.zip;  // undefined (not a crash)

// With a fallback using nullish coalescing
const country = apiResponse?.user?.profile?.address?.country ?? "Unknown";

Optional chaining (?.) and nullish coalescing (??) are modern JavaScript features — both part of the ECMAScript spec since 2020 — that make working with uncertain JSON structures much safer. Use them freely — they're supported in all modern browsers and Node.js 14+.

The replacer and reviver Functions

Both JSON.stringify() and JSON.parse() accept a second argument that lets you customise the transformation. These are genuinely useful for real-world scenarios:

js
// replacer: filter or transform values during stringify
const user = { name: "Alice", password: "s3cr3t", age: 30 };
const safe = JSON.stringify(user, ["name", "age"]); // only include these keys
// '{"name":"Alice","age":30}'  — password is excluded

// reviver: transform values during parse
const dateJson = '{"name": "Alice", "createdAt": "2024-01-15T09:30:00Z"}';
const parsed = JSON.parse(dateJson, (key, value) => {
  if (key === "createdAt") return new Date(value); // convert string to Date
  return value;
});
console.log(parsed.createdAt instanceof Date); // true
console.log(parsed.createdAt.getFullYear());   // 2024

The reviver pattern is especially useful for restoring Date objects after round-tripping through JSON, since JSON doesn't have a native date type.

Useful Tools

When working with JSON in JavaScript projects, these tools save time: JSON Formatter to pretty-print minified API responses, JSON Validator to check for syntax errors, JSON Path to query specific fields from large payloads, and JSON Escape when you need to embed JSON in a string. For more depth, the MDN JSON reference is excellent.

Wrapping Up

JSON.parse() and JSON.stringify() are the two functions you need. The key habits to build: always wrap parse() in try/catch, use optional chaining for nested access, and know what stringify() silently drops. Get those three right and you'll handle 99% of real-world JSON scenarios without surprises.