⚡Utilvy
2026-10-16

How to Fix "Unexpected token" in JSON: 10 Common Errors

The error "Unexpected token X in JSON at position 0" means something tried to parse a response as JSON, but the very first character it received was not valid JSON. In nine out of ten cases the server returned HTML, an empty body, or a plain-text message instead of JSON. Here are the ten most common causes and their fixes.

What the error actually means

JSON.parse() is extremely strict. It accepts exactly one JSON value — an object, array, string, number, boolean, or null — and nothing else. The message tells you two useful things:

  • The token it found: the actual character at the parse position, e.g. <, o, N, or \u0000.
  • The position: how many characters in the failure happened. Position 0 means the payload was never JSON at all; a position in the thousands means the JSON was valid up to that point.

Decoding the token usually points straight at the cause:

| Token found | What it reveals | Typical fix | |---|---|---| | < (position 0) | You got HTML: an error page, a 404 page, or the SPA's index.html | Check the URL and the response status before parsing | | o (position 1) | You received the string [object Object] — a JS object was stringified by mistake | Use JSON.stringify(obj), never string concatenation | | N (position 0) | The body was the text NaN or null was written as bare text | Guard nullable values before serializing | | \u0000 / \ufffd | BOM character or encoding corruption | Strip the BOM; fix the response charset | | } or , | Trailing comma or extra brace in hand-written JSON | Remove trailing commas — JSON forbids them | | ' | Single quotes used instead of double quotes | JSON requires double quotes around keys and strings | | u (position 1) | The body was undefined, so a variable was never assigned | Log the raw body; fix the missing assignment |

The 10 most common errors, ranked

  1. Parsing an HTML error page — the API returned 404/500 HTML, but the code called response.json() unconditionally. The most frequent cause by far.
  2. Fetching the SPA fallback — client-side routing intercepts /api/..., the server returns index.html, and the first character is <.
  3. Double-stringifying — calling JSON.parse() on a value that was never a string in the first place.
  4. Trailing commas — {"a": 1,} is legal JavaScript but illegal JSON. Hand-edited config files fail on this constantly.
  5. Single quotes — {'a': 1} comes from copying JavaScript object literals instead of real JSON.
  6. Empty response body — a 204 No Content or an empty 200 parsed as JSON.
  7. BOM prefix — UTF-8 files saved with a byte-order mark start with the invisible character \uFEFF, which JSON.parse rejects at position 0.
  8. [object Object] — building request bodies with string concatenation instead of JSON.stringify().
  9. Wrong Content-Type on the server — some clients refuse or garble the body before parsing; or a proxy (nginx, Cloudflare) injects its own HTML error page.
  10. Concurrency in local tooling — a dev server race leaves a partially written .json file that gets read mid-write.

A quick sanity check before anything else: print the first 200 characters of the raw body and the HTTP status code. In two minutes this identifies causes 1, 2, 6, and 7 without any further debugging.

Step-by-step debugging

Step 1 — Capture the raw response before parsing.

const res = await fetch(url);
const text = await res.text();
console.log(res.status, JSON.stringify(text.slice(0, 200)));

Step 2 — Classify the first character. < means HTML (fix the URL or handle the error status); empty means no body; anything readable-but-not-JSON means you hit causes 3–5 or 8.

Step 3 — Check the status code. Never parse the body of a failed response:

if (!res.ok) {
  throw new Error(`HTTP ${res.status}: ${text.slice(0, 120)}`);
}
const data = JSON.parse(text);

Step 4 — Strip a BOM if present. One line removes cause 7 permanently:

const clean = text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
const data = JSON.parse(clean);

Step 5 — For hand-written files, validate offline. Paste the file into a validator, or run node -e "JSON.parse(require('fs').readFileSync('data.json','utf8'))" — Node reports the exact line and column of the offending token, which turns a vague position number into a precise location.

Step 6 — Only after the payload is confirmed JSON, handle application logic. Wrapping the parse in try/catch and logging the raw text inside the catch block keeps this diagnostic information available in production, too.

"Unexpected token < in JSON at position 0"

Almost always an HTML page. Check whether the request URL is correct, whether a service worker or proxy rewrote it, and whether the server returned a redirect to a login page. If the endpoint exists but errors, read the HTML — the framework's error page usually names the actual server-side exception.

"Unexpected token o in JSON at position 1"

You parsed the string [object Object]. Somewhere an object was coerced to a string with "" + obj or template interpolation instead of JSON.stringify(obj). Search your code for JSON.parse( and confirm the argument is always a string produced by JSON.stringify, res.text(), or the server.

"Unexpected token } or , in JSON"

These come from JavaScript habits leaking into JSON files. Run the content through a formatter and remove trailing commas, swap single quotes for double quotes, and quote every key. If the JSON is large, a validator that reports line numbers saves far more time than reading character by character.

Make it a habit

Because JSON.parse fails on the first bad character, the fastest workflow is always the same: look at the raw text, decode the token with the table above, fix the producer of the payload — not the parser. When you need to inspect or reformat a suspicious payload, paste it into the free JSON formatter to pretty-print and validate it in one step; and if the payload turns out to be a base64-encoded string rather than JSON, the Base64 encoder and decoder will unwrap it first.

Try these tools