{ jsonzen }0 bytes uploaded
2026-06-15

JSON parse error: a complete debugging guide

Step-by-step workflow for debugging JSON.parse failures — including the surprising network-level causes most guides miss.


title: "JSON parse error: a complete debugging guide" slug: "json-parse-error-debugging-guide" date: "2026-06-15" description: "Step-by-step workflow for debugging JSON.parse failures — including the surprising network-level causes most guides miss." keywords:

  • JSON parse error
  • JSON debugging
  • JSON.parse failed
  • debugging JSON
  • JSON network error ogTitle: "JSON parse error · complete debugging guide · jsonzen"

Parse errors break in two layers: the JSON itself is malformed, or the way it was delivered is wrong. Most debugging guides only cover the first layer — they tell you about trailing commas and missing quotes, which matter, but they skip the delivery failures that account for a large share of real-world parse errors in production applications. This guide covers both, with a concrete end-to-end workflow you can follow the next time JSON.parse throws.

Anatomy of a parse error

When JSON.parse() receives input it cannot handle, it throws a SyntaxError. The error object has name ("SyntaxError"), a message, and a position number — a byte offset from the start of the string, not a line number. Position 0 is the first byte, and the offset doesn't reset per line. That's why a parse error at position 0 is almost never about the content of your JSON: something is happening before the first real character.

JavaScript engines report this differently:

  • V8 (Node.js, Chrome): SyntaxError: Unexpected token X in JSON at position N (older) or SyntaxError: Expected ',' or '}' after property value in JSON at position N (newer)
  • SpiderMonkey (Firefox): SyntaxError: JSON.parse: unexpected character at line X column Y of the JSON data
  • JavaScriptCore (Safari): SyntaxError: JSON Parse error: Unexpected identifier "X"

SpiderMonkey's format is the most useful because it gives line and column rather than a raw byte offset. V8's format is what most server-side code runs, so you're usually working with byte offsets. The JSON Validator normalises all of these into line/column regardless of which runtime produced the error.

Step 1: Check the raw response

Before you look at your JSON at all, check what the server actually sent. This is the step most developers skip, and it's the one that would have saved them the most time.

Do not trust your debugger's "Preview" tab. Chrome DevTools' Preview pane already interprets the response — it pretty-prints valid JSON and renders HTML. Either way, you are not seeing what the parser saw.

Instead: Network tab → click the failing request → "Response" tab (the plain text one, not Preview). This shows exactly what the server returned, byte for byte.

The most common surprise here is that the server returned HTML instead of JSON. An error page, a redirect page, a login wall — all HTML documents. Their first character is <. Your parser throws immediately at position 0: SyntaxError: Unexpected token < in JSON at position 0.

If you see HTML in the Response tab, your fix is on the server side. No amount of tweaking your parse code will help until the server sends the right content.

Step 2: Inspect the Content-Type

Even when the response body is correct JSON, mismatched headers can cause a parse error indirectly.

fetch() does not automatically decide to parse JSON based on what the server sends. If you call response.json(), it tries to parse the body as JSON regardless of what Content-Type the server declared — so a server sending HTML with Content-Type: text/html will still produce a parse error if your code calls .json() on it.

Before parsing, log this:

const response = await fetch(url);
console.log(response.headers.get("content-type"));
// Should be: "application/json" or "application/json; charset=utf-8"
const data = await response.json();

If the content-type is text/html, text/plain, or anything non-JSON, treat it as a server-side bug — the endpoint is not behaving correctly. The content-type mismatch is a symptom, not the root cause.

A subtler variant: the server sends Content-Type: application/json; charset=utf-16. If your code reads the response as UTF-8 bytes (which is the default for fetch), the encoding mismatch will scramble every character and produce a garbage string that fails to parse. Charset problems of this kind show up as parse errors with bizarre positions in the middle of what looks like valid JSON.

Step 3: Look for hidden characters

Assuming the response is JSON-shaped and the content-type looks right, the next place to look is invisible characters that are corrupting the input before your parser sees it.

Byte order mark (BOM). Files saved as "UTF-8 with BOM" (the Windows default in many editors and some APIs) prepend the three bytes EF BB BF to the content. To JSON.parse, this looks like a garbage character before the opening {. The error will say position 0, and it will seem inexplicable because the JSON looks perfectly fine when you paste it into a text editor (which typically hides the BOM). To detect it: text.charCodeAt(0) === 0xFEFF returns true when a BOM is present. To strip it: text = text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text.

Zero-width space (U+200B). This is the most insidious invisible character. It is a common passenger on text copied from web pages, documentation, Notion, and Confluence. It is invisible in virtually every editor and terminal. It will cause a parse error wherever it lands — inside a key, inside a value, after a colon — and there will be no visible indication of where it is. Detection: text.includes('​'). For a quick hex-level look at a file, run xxd file.json | head on Linux/Mac (or Format-Hex file.json | Select-Object -First 5 in PowerShell on Windows) and look for e2 80 8b in the output.

Trailing newlines and whitespace. These are almost always fine — JSON.parse ignores leading and trailing whitespace per the spec. But some stricter validators or non-standard parsers treat them as errors. If you're using a library other than native JSON.parse, check its documentation.

The quick sanity check: text.charCodeAt(0) should return a value corresponding to { (123), [ (91), " (34), a digit (48–57), t (116), f (102), or n (110) — the only valid first characters in a JSON document. Anything else means something is prepended.

Step 4: Validate locally

Once you have the raw response text, paste it into the JSON Validator. It will tell you the exact line and column of the first error, which is far more useful than a byte offset.

If the validator says "Valid JSON", the problem is environmental: the string you're validating is not the same string the parser is seeing at runtime. Common reasons this happens:

  • String mutation between network and parser. Some middleware, logging library, or proxy is transforming the response body before it reaches your parse call.
  • Encoding mismatch. You're viewing the content in a different encoding than the parser is using, so you see clean JSON but the parser sees garbled bytes.
  • Race condition. The response you're looking at in DevTools is from a different request than the one that failed — the data changed between requests.

If the validator says "Valid JSON" and the parser still throws, add a temporary console.log(JSON.stringify(text.slice(0, 20))) immediately before the JSON.parse call to see the exact first 20 characters as a serialised string, escape sequences and all. This makes invisible characters visible.

Step 5: Common API-side culprits

If steps 1–4 haven't found the problem, the cause is likely in how the JSON was produced rather than how it's being delivered.

Debug output prepended to the response. A console.log, var_dump, print_r, or stack trace that fires before the response body will prepend text to what the client receives. In serverless environments (AWS Lambda, Vercel Functions), stdout is typically separated from the HTTP response body — but some frameworks capture stdout and prepend it. If you see a parse error where the response starts with what looks like a log message, this is the cause.

Base64-wrapped JSON. Some APIs — particularly those that deal with binary data or that want to avoid encoding issues — return base64-encoded JSON rather than raw JSON. The payload looks like a long string of alphanumeric characters and equals signs. Your code needs to decode it first: JSON.parse(atob(responseText)) in the browser, or JSON.parse(Buffer.from(responseText, 'base64').toString('utf8')) in Node. If you're seeing parse errors on what appears to be a base64 string, this is what's happening.

NDJSON / newline-delimited JSON. NDJSON (Newline Delimited JSON) is a format where each line is a separate, independent JSON object. The whole response body is not valid JSON — JSON.parse on the whole thing will always fail. Parse it line by line: text.split('\n').filter(Boolean).map(JSON.parse). Streaming APIs (OpenAI's streaming completions, server-sent events, log aggregation APIs) commonly use this format.

Truncated response. If the connection drops mid-payload, you receive a partial JSON document. The parse error will be at or near the end of the string, with the position close to the total length. Before parsing, check that the response completed successfully: response.ok and response.status === 200 in fetch. Also check that the Content-Length header (if present) matches the actual body length.

Step 6: When the data is the problem

If the above steps all check out and the JSON itself is genuinely malformed, the approach depends on where the JSON comes from.

Developer-controlled JSON (config files, test fixtures, hand-written data): fix at source. The JSON Validator pinpoints the exact line and column. Common culprits are trailing commas, single quotes, and unquoted keys — all covered in detail in the companion posts on fixing invalid JSON and Unexpected token errors.

LLM-generated JSON or scraped data: these sources produce structurally near-valid JSON that fails on small issues — a trailing comma, a missing bracket, an unescaped quote. For these, use JSON Repair as a preprocessing step before JSON.parse. It fixes the mechanical issues deterministically without losing data.

Third-party APIs you don't control: validate at the boundary and fail loudly. Don't let a malformed response from an external API propagate deep into your application where the error is harder to trace. A minimal pattern:

let data;
try {
  data = JSON.parse(responseText);
} catch (e) {
  throw new Error(`API at ${url} returned invalid JSON: ${e.message}`);
}

For more rigorous validation, pair this with a schema validator (Zod, JSON Schema) to catch responses that parse successfully but have the wrong shape.

Quick reference: where to look first

Match the symptom to the likely cause:

  1. "Position 0" — BOM prepended to the file or response; server returned HTML (first char is <); base64-wrapped response that hasn't been decoded; invisible character (U+200B, curly quote) at the very start; content-type mismatch causing encoding corruption.

  2. "Position == length of payload" or near the end — Truncated response; connection dropped mid-transmission; NDJSON parsed as a single document (last line has no closing }).

  3. Mid-payload position — Trailing comma, missing comma, unescaped character, or structural mismatch (mismatched braces/brackets). The actual mistake is typically 0–10 characters before the reported position.

  4. Bizarre mid-payload position with no obvious error at that point — Encoding mismatch (UTF-16 read as UTF-8, or vice versa); invisible character embedded in a key or value; BOM mid-document from two JSON files that were concatenated.

  5. Validator says valid, parser still throws — Environmental issue: string mutation between read and parse, wrong variable being parsed, stale cached response. Add console.log(JSON.stringify(text.slice(0, 20))) immediately before JSON.parse to see the actual first characters.

TL;DR

When JSON.parse fails, work through this checklist:

  • [ ] Check the raw response (Network → Response tab, not Preview). Is it actually JSON, or is it HTML?
  • [ ] Check Content-Type. Log response.headers.get("content-type") before parsing.
  • [ ] Check position 0. text.charCodeAt(0) should be {, [, ", a digit, t, f, or n. Anything else means something is prepended.
  • [ ] Scan for a BOM (charCodeAt(0) === 0xFEFF) or zero-width spaces (text.includes('​')).
  • [ ] Paste the raw text into the JSON Validator to get line/column rather than a byte offset.
  • [ ] If the validator says valid: add console.log(JSON.stringify(text.slice(0, 20))) before JSON.parse to confirm you're parsing what you think you're parsing.
  • [ ] Check for API-side issues: prepended log output, base64 encoding, NDJSON format, truncated response.
  • [ ] If the JSON is genuinely broken: fix at source for developer-owned data; use JSON Repair for LLM or scraped data; validate at the boundary for third-party APIs.