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
- Parsing an HTML error page — the API returned 404/500 HTML, but the code called
response.json()unconditionally. The most frequent cause by far. - Fetching the SPA fallback — client-side routing intercepts
/api/..., the server returnsindex.html, and the first character is<. - Double-stringifying — calling
JSON.parse()on a value that was never a string in the first place. - Trailing commas —
{"a": 1,}is legal JavaScript but illegal JSON. Hand-edited config files fail on this constantly. - Single quotes —
{'a': 1}comes from copying JavaScript object literals instead of real JSON. - Empty response body — a 204 No Content or an empty 200 parsed as JSON.
- BOM prefix — UTF-8 files saved with a byte-order mark start with the invisible character
\uFEFF, whichJSON.parserejects at position 0. [object Object]— building request bodies with string concatenation instead ofJSON.stringify().- 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.
- Concurrency in local tooling — a dev server race leaves a partially written
.jsonfile 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.