There is one error that every developer who has touched an API has seen:
SyntaxError: Unexpected token < in JSON at position 0It is worth knowing two things about it immediately. First, the fix is almost never in your JSON — there is no JSON. Second, that exact message is out of date, and if you are still seeing it you are on an old runtime.
What `<` actually tells you
Position 0 means the very first character. The first character is <. Nothing in the JSON grammar starts with <, but one very common thing does: HTML.
Your code asked for JSON and received a web page. The usual culprits, in rough order of frequency:
Confirm it in one command
Stop guessing at the response and look at it:
# Status and content type, without the body getting in the way
curl -s -o /dev/null -D - https://api.example.com/items | head -20
# The first bytes of whatever actually came back
curl -s https://api.example.com/items | head -c 300If the second command prints <!DOCTYPE html>, you have your answer and you can stop debugging the parser.
The fix in code
const res = await fetch(url);
if (!res.ok) {
// Read as text — the body is probably an error page, not JSON.
throw new Error(`HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`);
}
const type = res.headers.get('content-type') || '';
if (!type.includes('application/json')) {
throw new Error(`Expected JSON, got ${type}: ${(await res.text()).slice(0, 200)}`);
}
return res.json();Those two guards turn an unreadable Unexpected token < into an error that names the status and shows you the first 200 characters of what actually arrived. That is the difference between a five-minute fix and an afternoon.
The message has improved — yours may be stale
V8 rewrote these errors, so modern Node and Chrome tell you far more. Same bad input, two runtimes:
The new one quotes the offending content back at you, which is exactly the information the old one withheld. If you are searching for the old string and finding only old answers, that is why — and it is a hint that upgrading your runtime would make every future parse failure easier to read.
Every other message, and what it means
Measured on Node 22. These cover essentially all real-world parse failures:
That [object Object] row catches people repeatedly. It means the value was already turned into a string by template interpolation or String() before it reached the parser — nothing is wrong with the data, only with where it was stringified.
Getting a better error than the runtime gives you
Even the improved V8 messages describe the grammar rather than the mistake. Pasting the document into the JSON validator gives the same failures in plainer terms, with a line, a column and a suggested fix:
That last one is the case worth having a tool for. A bracket closed in the wrong order inside a large document is nearly impossible to find by eye, and the runtime will only tell you that something went wrong somewhere.