skip to content

JSON.parse() fails on text it cannot read. What exactly does it do on malformed input, what kinds of real-world input trigger it, and how should production code handle those failures?

level: seniorimportance: should knowfreq 45%

answer

  1. it throws, it does not return a flag
  2. SyntaxError, thrown synchronously
  3. the argument is coerced to text first
  4. classic culprit: an HTML error page
  5. success means well-formed, not correct

basics

~20 s

JSON.parse throws a SyntaxError synchronously on any text that is not valid JSON — a truncated body, an HTML error page, a trailing comma, a comment. Wrap the call in try/catch at the boundary and report the failure with context.

solid answer

~50 s

`JSON.parse` is all-or-nothing: given text that does not match the JSON grammar it throws a `SyntaxError` and returns nothing partial. Because the throw is synchronous, an ordinary `try`/`catch` around the call catches it. The grammar is stricter than most people remember — no trailing commas, no comments, no single-quoted strings, no unquoted keys, no `NaN` or `Infinity` literals, and a leading byte-order mark is not whitespace and fails too. The argument is also coerced to a string first, which is why `JSON.parse(undefined)` throws while `JSON.parse(null)` quietly returns `null`. In production the usual culprits are not malformed data at all: a proxy or error page returning HTML, an empty body, or a response cut short. So parse at a boundary where you can attribute blame, log a bounded prefix of the raw text along with its length, distinguish a transport failure from a parse failure, and never branch on the error message — its wording is engine-specific.

code

javascript · 25 lines
javascript
function parseJson(text, source) {
  try {
    return { ok: true, value: JSON.parse(text) };
  } catch (err) {
    if (!(err instanceof SyntaxError)) throw err;
    const raw = String(text);
    return {
      ok: false,
      source,
      reason: err.message,      // human evidence only, never branched on
      length: raw.length,
      head: raw.slice(0, 80)    // bounded prefix diagnoses the incident
    };
  }
}

console.log(parseJson('{"a":1}', 'cache').ok);              // true
console.log(parseJson('<!DOCTYPE html><html>', 'api').head); // <!DOCTYPE html><html>
console.log(parseJson('{"a": 1,}', 'api').ok);              // false — trailing comma
console.log(parseJson('', 'api').ok);                       // false — empty body
console.log(parseJson('{"a":1}', 'file').ok);         // false — leading BOM

// Coercion of the argument, not a special case:
console.log(JSON.parse(null));                              // null
// JSON.parse(undefined) throws: the text "undefined" is not JSON

go deeper

for a junior

Know that malformed text makes JSON.parse throw rather than return a falsy value, that the error is a SyntaxError, and that a plain try/catch around the call is what handles it.

for a middle

Explain the mechanics: the throw is synchronous with no partial result, the argument is coerced to a string first, and the grammar rejects trailing commas, comments, unquoted keys and a leading byte-order mark.

for a senior

Show you have debugged this in production — name the real causes such as an HTML error page or a truncated body, log a bounded prefix plus the length, separate transport failure from parse failure, and never branch on the engine's message text.

for a principal

Own where the boundaries sit: which components are allowed to parse untrusted text, what every parse failure must emit for an on-call engineer to diagnose it, and how failures are surfaced consistently instead of each team inventing its own fallback.

## What actually happens `JSON.parse(text)` either returns a fully-built value or throws. There is no partial result, no `null` return, no error flag: if the text does not match the JSON grammar from the first character to the last, the call throws a **`SyntaxError`** — the same built-in error type the engine uses for source it cannot parse. The throw is synchronous, on the same call stack, so the handling is an ordinary `try`/`catch`: ```js let value; try { value = JSON.parse(text); } catch (err) { // err instanceof SyntaxError } ``` A detail that catches people out: the argument is coerced to a string before parsing. So `JSON.parse(null)` becomes the text `"null"` and returns `null` with no complaint, while `JSON.parse(undefined)` becomes the text `"undefined"`, which is not valid JSON, and throws. An accidental `undefined` reaching the parser surfaces as a syntax error rather than as the missing-value error you would expect. ## The grammar is stricter than people remember JSON is not JavaScript object literal syntax. All of the following throw: ```js JSON.parse("{'a': 1}"); // single-quoted string JSON.parse('{a: 1}'); // unquoted key JSON.parse('{"a": 1,}'); // trailing comma JSON.parse('{"a": 1} // x');// comment JSON.parse('{"a": NaN}'); // NaN is not a JSON literal JSON.parse(''); // empty text is not a JSON value JSON.parse('{"a":1}');// leading byte-order mark ``` The byte-order-mark case is worth memorising because it is invisible in a terminal and in most editors. Only space, tab, carriage return and line feed count as whitespace in JSON; U+FEFF does not, so a file saved as "UTF-8 with BOM" fails to parse while looking perfectly correct on screen. ## What actually breaks in production In real incidents the input is usually not slightly-wrong JSON — it is not JSON at all: - **An HTML error page.** A gateway, load balancer, captive portal or authentication redirect returns a styled 502 or login page. The text starts with `<`, and the parse fails at position 0. This is far and away the most common cause. - **An empty body.** A 204 response, a closed connection, or a handler that returned nothing. - **A truncated payload.** The connection dropped mid-stream, a size limit clipped the body, or a log line was cut. The text is valid JSON right up to the point where it stops. - **A plain-text error.** A proxy writing `upstream timed out` or a health-check probe answering `OK`. - **A wrong encoding or a stray BOM** on a file read from disk. The pattern in all of these is the same: something upstream substituted a different content type for the one you expected, and the parser is simply where you find out. ## Handling it well **Parse at a boundary.** Do the parsing at the edge where you know what the text was supposed to be and who produced it, and hand the rest of the system a value. A `SyntaxError` escaping from deep inside business logic tells nobody anything. **Catch narrowly and rethrow the rest.** Inside the `catch`, confirm you are handling a parse failure rather than swallowing an unrelated bug: ```js try { return JSON.parse(text); } catch (err) { if (!(err instanceof SyntaxError)) throw err; // handle the parse failure } ``` **Log evidence, bounded.** The `SyntaxError` message tells you what the parser choked on; what you actually need is what arrived. Record the text's length and a short prefix — the first 80 or so characters — plus the source and, where you have it, the declared content type. A prefix of `<!DOCTYPE html>` diagnoses the incident instantly. Keep the prefix bounded so a large body cannot flood the log, and be deliberate about payloads that may contain sensitive fields. **Never branch on the message text.** The wording, and whether it mentions a position or an offending token, is engine-specific and changes between versions. Use the error *type* for control flow and treat the message as human-readable evidence only. **Do not confuse a transport failure with a parse failure.** "Could not reach the service" and "the service answered with something that is not JSON" have different causes, different owners and different retry semantics. Keep them as separate outcomes rather than collapsing both into one generic error. **Decide what happens next, explicitly.** A helper that returns a discriminated result — success with a value, or failure with diagnostics — forces every caller to make that decision instead of silently receiving `null`. Returning a default on failure is a legitimate choice for a cache read and a bad one for a payment callback; the point is that it be a choice. ## One thing parsing does not give you Success means only that the text was well-formed. It says nothing about whether the value has the fields you need, or the right types in them. Structural checking of the parsed value is a separate step, and code that treats a successful parse as confirmation of shape simply moves the failure a little further downstream.

  • Why does JSON.parse(undefined) throw while JSON.parse(null) returns null?
    Because the argument is coerced to a string before parsing. `null` becomes the text `"null"`, which is a valid JSON literal, so you get `null` back. `undefined` becomes the text `"undefined"`, which is not part of the JSON grammar, so it throws a SyntaxError. Neither is a special case — both are just what the coerced text happens to be.
  • A parse fails at the very start of the text with a complaint about the character '<'. What is your first hypothesis?
    That the response was never JSON. Something upstream — a gateway, load balancer, authentication redirect or error page — returned HTML where the service should have answered, so the body begins with `<!DOCTYPE` or `<html>`. Confirm by logging a bounded prefix of the raw text and the declared content type; the fix is upstream, not in the parsing code.
  • Should error handling branch on the SyntaxError's message?
    No. The wording, and whether it names a position or an offending token, is engine-specific and changes across versions, so message matching is fragile by construction. Branch on the error type with `instanceof SyntaxError`, keep the message as human-readable evidence in the log, and carry your own structured context — source, length, prefix — for anything the code needs to decide.
  • If JSON.parse succeeds, what have you actually established about the data?
    Only that the text was well-formed JSON. Nothing about whether the expected fields are present, whether they hold the expected types, or whether the numbers are in range. Checking the structure of the parsed value is a separate step; treating a successful parse as confirmation of shape just relocates the failure to whichever line first touches a missing property.

saying these in an interview costs you the question

  • Thinks JSON.parse returns null or undefined on bad input
  • Expects a partial object for a truncated payload
  • Matches on the error message text to classify failures
  • Assumes JSON allows trailing commas or comments like JavaScript
  • Treats a successful parse as proof the data has the right shape
  • Catches everything around the parse and swallows unrelated bugs

context