Fixing Unexpected Token JSON Parse Errors
You call an API, hand the response to JSON.parse, and get "Unexpected token" thrown back at you. The frustrating part is that the error points at a character, not a cause, and most of the time the JSON is not actually the problem. The string you passed in was never JSON to begin with. This guide covers how to read the error, what each variant tells you, and how to fix the cause instead of guessing at commas.
The Error Message Changed
If you learned this error a few years ago, you remember it looking like Unexpected token < in JSON at position 0. V8 rewrote these messages, so on Node 20+ and current Chrome you now get:
This is a real improvement, because the new format echoes back the first stretch of the input. That snippet is the single most useful thing on your screen. Old message or new, the diagnosis is the same, but the new one usually hands you the answer without a debugger.
Firefox and Safari still word it differently (JSON.parse: unexpected character at line 1 column 1), so do not pattern-match on the message text in code. Match on the input instead.
A Leading Angle Bracket Means HTML
Unexpected token '<' at the very start is the most common version of this error, and it almost never means your JSON is malformed. It means the server sent HTML. Your fetch hit a 404 page, a login redirect, a proxy error page, or a framework error screen, and the body starts with or .
The reason it surprises people is that response.json() does not check the status code. A 500 response with an HTML error page is still a resolved promise:
Check res.ok and the content type before parsing and this class of bug reports itself properly instead of surfacing as a parse error three functions away. A leading { in the echoed snippet means the opposite: the payload really is JSON-shaped and something inside it is wrong.
Almost-JSON That Engines Reject
The second family of failures is input that looks like JSON to a human but violates the spec. JSON is stricter than JavaScript object literals, and stricter than Python dicts. These are the ones that actually trip developers up:
| Input | What JSON.parse says | Why |
|---|---|---|
{"a": 1,} | Expected double-quoted property name at position 8 | Trailing commas are illegal |
{a: 1} | Expected property name or '}' at position 1 | Keys must be quoted |
{'a': 1} | Expected property name or '}' at position 1 | Single quotes are not JSON |
{"a": undefined} | Unexpected token 'u' | undefined is not a JSON value |
{"a": NaN} | Unexpected token 'N' | NaN and Infinity are not either |
{"a": True} | Unexpected token 'T' | JSON booleans are lowercase |
NaN one usually comes from a serializer that stringified a computed value without checking it. Comments are worth calling out separately: // like this is valid in JSONC and in your tsconfig.json, but plain JSON.parse rejects it. A config file that opens fine in VS Code can still fail in your build script.
Most of these are mechanically fixable. Paste the broken text into the JSON validator and it will point at the failing line and repair the fixable classes for you, converting single quotes, quoting bare keys, and stripping trailing commas and comments. It will not invent a value for undefined or NaN, because there is no correct JSON representation of either. Those you have to fix at the source.
Unexpected End of JSON Input
Unexpected end of JSON input has no position number because the parser ran out of characters while still expecting more. Three causes account for nearly all of it.
An empty string is the most common. A 204 No Content response, or a 200 with an empty body, gives you "", and JSON.parse("") throws. Guard it:
Reading text() first also gives you the raw body to log when parsing fails, which is worth doing on every API client you write.
The other two causes are a truncated response, where a stream closed early or a proxy cut off a large payload, and an unbalanced brace in a file you edited by hand. For the hand-edited case, run the file through the JSON formatter: a correctly nested document reindents cleanly, and a missing closing brace shows up immediately as indentation that never returns to the left margin.
Trailing Content After Valid JSON
Unexpected non-whitespace character after JSON at position 7 means the parser read one complete value and then found more text. Usually you have NDJSON, one object per line, from a log file or a streaming endpoint:
That is not one JSON document, it is two. Split on newlines and parse each line separately. The other cause is a duplicated response body from a handler that wrote output twice, which is a server bug, not a client one.
Double-Encoded JSON
This one is nastier because it does not throw. If a service stringifies an already-serialized payload, you get "{\"a\":1}", and parsing that succeeds and returns a string. Your code then fails later with something unhelpful like "cannot read property of undefined."
Do not leave that double-parse in production code as a permanent fix. Use it to confirm the diagnosis, then get the encoding fixed upstream. If you are chasing the escaping in the other direction, building a JSON string to embed in another string, the JSON escape tool shows you what the encoded form should look like.
Debug the Input, Not the Parser
The habit that saves the most time: when parsing fails, log the raw text and its length, not the parsed result you do not have. console.log(JSON.stringify(text.slice(0, 200))) reveals a leading byte-order mark, a stray whitespace prefix, or an HTML doctype in one line. A BOM is invisible in your terminal and in most editors, and it throws Unexpected token at position 0 exactly like HTML does. Strip it with text.replace(/^\uFEFF/, '').
For anything longer than a screenful, paste the body into the JSON validator to get the failing line and column, then into the beautifier once it parses so you can see the structure. Nine times out of ten the fix is not in your JSON at all. It is a missing status check on the request that produced it.