Why my JSON parse fails (Unexpected token) — 3 causes, none of them in the JSON

You see this in the console:
SyntaxError: Unexpected token ` in JSON at position 0
And you go hunting for the bug in your JSON. Wrong place.
Position 0 is the very first character. The parser tripped before it started working: it never reached a quote or a comma. That means one thing — it was handed not JSON. Not JSON at all, from the first character on.
So the first move is always the same. Don't stare at your code — print what you're parsing:
console.log(JSON.stringify(raw.slice(0, 100)))
JSON.stringify specifically, not a plain console.log: it reveals the invisible — newlines, quotes, whitespace. The first hundred characters almost always name the culprit. And in the error message, read the character itself first: it's the clue. Three cases cover nearly everything.
Cause 1. The model wrapped the JSON in markdown
The most common one if there's AI in your project. You asked the model for JSON — it gave you JSON. Helpfully, in a syntax-highlighted block:
```json
{"name": "Ann", "age": 30}
```
Inside: perfect JSON. Outside: three backticks and the word json. To JSON.parse that's garbage from the very first character. Hence "Unexpected token `".
The model isn't malfunctioning. It was trained on markdown, where code is conventionally fenced. It's doing what people do.
How to confirm: a backtick in the error message is the whole diagnosis.
How to fix. The stopgap is to strip the wrapper:
const clean = raw.trim().replace(/^```(?:json)?\s*/i, '').replace(/```$/i, '')
const data = JSON.parse(clean)
It works, but it treats the symptom. The model may wrap differently, add "Here's your JSON:" before the block, or append a note after it. You'll be patching that regex forever.
The real fix isn't asking for JSON in words — it's demanding it at the API level: structured output forces the model to return an answer that matches your schema. Then there is no wrapper at all, and nothing to strip.
Cause 2. The server sent HTML instead of data
The error looks like this:
SyntaxError: Unexpected token < in JSON at position 0
An angle bracket at the start is <!DOCTYPE html>. You got a web page.
What happened: the request went somewhere wrong or failed, and the server politely returned a human-readable error page — a 404 or a 500. Your code saw a response, got excited, and tried to parse the page as data.
How to confirm: open the Network tab in your browser — check the status and the Response tab. There's your page.
How to fix — stop swallowing errors:
const res = await fetch(url)
if (!res.ok) throw new Error(`${res.status} at ${url}`)
const ct = res.headers.get('content-type') ?? ''
if (!ct.includes('application/json')) {
throw new Error(`Expected JSON, got ${ct}: ${(await res.text()).slice(0, 200)}`)
}
const data = await res.json()
Five lines — and instead of a cryptic Unexpected token < you get "404 at /api/usres". With the typo right there in front of you.
There's a close relative next door: Unexpected token u. The u is the start of undefined — you're parsing a variable that holds nothing. Usually because you forgot an await. That's its own classic, covered here: why fetch returns undefined.
Cause 3. It is JSON, but by someone else's rules
If you got this far, you really were handed an object, and the error points somewhere in the middle rather than at position 0. Now you can look at the text itself.
JSON is stricter than it looks. It doesn't forgive what JavaScript happily eats and what text editors love to insert:
- Single quotes.
{'name': 'Ann'}isn't JSON. Double quotes only. - Trailing comma.
{"a": 1,}is legal in JS, an error in JSON. - Smart quotes.
{"name": "Ann"}with typographic curly quotes instead of"— the curse of everyone who copies from a doc or a chat app. Your eye can't tell; the parser falls over. - Comments. JSON doesn't have them at all.
How to confirm: drop the text into any JSON validator — it'll point at the line and character.
How to fix: if you generate the JSON — never build it by string concatenation. Build an object and hand it to JSON.stringify; it won't forget a quote. If the JSON came from outside, the sender is at fault — fix it at the source, not with regexes on your end.
The order of operations
A short cheat sheet:
- Print the first 100 characters through
JSON.stringify. - Look at the character from the error:
`→ model wrapper,<→ HTML,u→ undefined. - Position 0 means the problem is outside the JSON. Not position 0 — now the validator is worth opening.
- Fix the cause, not the text. Wrapper — turn on structured output. HTML — check the response status. Broken quotes — generate through
stringify.
The thing worth taking away: JSON.parse almost never lies. It honestly says "this isn't JSON" — and it's usually right.
Why does the model add ```json when I explicitly asked for "JSON only"?
Because asking in words is a wish, not a guarantee. The model predicts plausible text, and in the text it learned from, JSON almost always sits in a code block. Sometimes it obeys, sometimes it doesn't — why the model ignores your instructions. Only a schema at the API level guarantees anything.
Should I just wrap JSON.parse in try/catch?
Absolutely — but that's armor, not a diagnosis. try/catch keeps your app from crashing; just make sure the catch block logs the actual text that failed to parse. Otherwise you know "something broke" and nothing else.
Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.





