Fix JSON.parse Unexpected Token Errors: Every Cause
· by webmaster
SyntaxError: Unexpected token from JSON.parse means the text you passed in is not JSON. The token in the message tells you what kind of text it is instead, and that narrows the cause to a short list. This guide covers each cause, what produces it, and the fix. Every example was run on Node 22, which shares its V8 engine with Chrome, so the messages match what you see in DevTools.
When you are unsure, paste the raw text into the JSON Formatter. It points at the exact position of the first invalid character, which is faster than reading a long string by eye.
V8 changed the error wording in 2023
V8 rewrote its messages in 2023. Node 20+ and current Chrome use the new wording, and older runtimes use the old. The problems are the same, only the sentences differ.
| Situation | Old message | New message |
|---|---|---|
| HTML page | Unexpected token < in JSON at position 0 | Unexpected token '<', "<!DOCTYPE "... is not valid JSON |
| Single quotes | Unexpected token ' in JSON at position 1 | Expected property name or '}' in JSON at position 1 |
| Trailing comma | Unexpected token } in JSON at position 8 | Expected double-quoted property name in JSON at position 8 |
| Empty string | Unexpected end of JSON input | Unexpected end of JSON input |
Firefox says JSON.parse: unexpected character at line 1 column 1 of the JSON data. Safari’s messages start with JSON Parse error:. The causes below apply to all of them.
Unexpected token < means you received HTML
The string starts with <, so you got HTML instead of JSON. This is the most common cause by a wide margin. The JSON is rarely the problem here, because the response contains no JSON at all.
JSON.parse('<!DOCTYPE html><html>');
// SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
A server sends HTML when you asked for JSON for one of these reasons:
- Wrong URL: you got a 404 page.
- Expired session: you were redirected to a login page.
fetchfollows redirects without telling you, so the status is 200 and the body is HTML. - Server error: the server rendered a 500 page.
- SPA fallback: a single-page app serves
index.htmlfor every unknown path, including a misspelled API route. - PHP output: a warning or notice was printed before the JSON.
The login redirect is the hardest to spot, because the status looks healthy until you read the body. Inspect the response before you parse it. The debugging section below shows how.
Unexpected token ’ comes from single quotes
JSON requires double quotes around strings and keys. JavaScript object literals accept single quotes, so this error usually means someone typed the “JSON” by hand or a template used the wrong quote character.
JSON.parse("{'name': 'Ada'}");
// SyntaxError: Expected property name or '}' in JSON at position 1
Fix the source. If you cannot, and the text is a JavaScript literal rather than JSON, the JSON to JavaScript converter shows the two formats side by side. Never repair it with eval(). eval() executes arbitrary code, and the input may one day come from someone else.
Unquoted keys give the same message:
JSON.parse('{a: 1}');
// SyntaxError: Expected property name or '}' in JSON at position 1
Trailing commas are valid JavaScript but invalid JSON
A comma after the last item is valid JavaScript, and many linters prefer it. JSON rejects it.
JSON.parse('{"a": 1,}');
// SyntaxError: Expected double-quoted property name in JSON at position 8
JSON.parse('[1, 2,]');
// SyntaxError: Unexpected token ']', "[1, 2,]" is not valid JSON
This error usually comes from JSON built by string concatenation in a loop, where the last iteration leaves a comma behind. Build an object and call JSON.stringify() instead of concatenating strings. The same advice holds in every language.
Comments are not allowed in JSON
JSON has no comment syntax, neither // nor /* */.
JSON.parse('{"a": 1 // note\n}');
// SyntaxError: Expected ',' or '}' after property value in JSON at position 8
Config files that look like JSON but carry comments (tsconfig.json, VS Code settings) are JSONC or JSON5, and they need their own parser. Strip the comments, or use the parser that matches the format.
Unexpected end of JSON input means an empty or cut-off string
The parser reached the end of the string before the structure closed. Either the string is empty, or it was cut off.
An empty string comes from a 204 No Content response, a body that was never written, or an undefined that was coerced to a string somewhere.
JSON.parse('');
// SyntaxError: Unexpected end of JSON input
A truncated string comes from a dropped connection, a proxy that enforced a body-size limit, a clipped log line, or a database column too short for the value.
JSON.parse('{"a": [1, 2');
// SyntaxError: Expected ',' or ']' after array element in JSON at position 11
For the empty case, check response.status and the Content-Length header before parsing. For truncation, compare the length you received with what the server sent.
NaN, Infinity and undefined are not JSON values
These are JavaScript values, and JSON has no representation for them. JSON.stringify already turns them into null (or drops the key entirely for undefined), so a string that contains them did not come from a JSON serializer. It was built by hand or produced by another language.
JSON.parse('{"n": NaN}');
// SyntaxError: Unexpected token 'N', "{"n": NaN}" is not valid JSON
JSON.parse('{"u": undefined}');
// SyntaxError: Unexpected token 'u', "{"u": undefined}" is not valid JSON
JSON.stringify({ n: NaN, u: undefined, i: Infinity });
// '{"n":null,"i":null}'
Python is the usual source. json.dumps writes NaN and Infinity by default, and Python’s own json.loads accepts them back, so the bug stays hidden until a JavaScript client reads the data. If a Python service feeds a browser, call json.dumps(data, allow_nan=False) so it fails on the server, where you can see it.
A BOM at the start of the file breaks JSON.parse
A UTF-8 byte order mark (EF BB BF) is invisible in most editors, but JSON.parse does not count it as whitespace. It arrives with files saved by Windows Notepad, PHP files that emit output before <?php, and some CSV-to-JSON exports.
JSON.parse('{"a": 1}');
// SyntaxError: Unexpected token '', "{"a": 1}" is not valid JSON
JSON.parse('{"a": 1}'.replace(/^/, ''));
// { a: 1 }
The sign is a token in the error message that looks blank. Strip it before parsing and save the file without a BOM. Node’s fs.readFileSync(path, 'utf8') keeps the BOM, while response.text() strips it during decoding, so this error shows up mostly with files rather than HTTP responses.
Double-encoded JSON parses to a string
Here the parse succeeds, but the result is a string instead of an object. Something called JSON.stringify twice, or a database stored a JSON string in a JSON column and the driver encoded it again on the way out.
const once = JSON.stringify({ a: 1 }); // '{"a":1}'
const twice = JSON.stringify(once); // '"{\\"a\\":1}"'
typeof JSON.parse(twice); // 'string'
JSON.parse(JSON.parse(twice)); // { a: 1 }
A value that starts with "{ and is full of \" is the telltale sign. Parsing twice works around it, but the real fix is upstream: serialize once. The same shape appears with JSON inside a Base64 field, such as a JWT payload. Decode the outer layer first with the Base64 Decoder, then parse the text.
How to debug any JSON.parse error
Read the response as text before you parse it. response.json() parses the body and discards the raw text, which leaves you with only the error message.
const res = await fetch('/api/items');
const text = await res.text();
console.log(res.status, res.headers.get('content-type'));
console.log(text.slice(0, 200));
if (!res.ok) throw new Error(`HTTP ${res.status}`);
if (!res.headers.get('content-type')?.includes('application/json')) {
throw new Error('Expected JSON, got: ' + text.slice(0, 80));
}
const data = JSON.parse(text);
Three checks catch nearly every case:
- Status code: a 401, 404 or 500 should never reach
JSON.parse. - Content-Type:
text/htmlmeans a page, andapplication/jsonmeans the server at least tried. - First 200 characters: the
<!DOCTYPE, the BOM, the PHP warning or the second layer of quotes is visible right away.
In the browser, the Network tab shows the raw body without any code change. On the server, curl -i prints headers and body together.
For input you do not control, a small tolerant wrapper can strip the BOM and surrounding whitespace before parsing:
function parseJsonSafe(text) {
return JSON.parse(text.replace(/^/, '').trim());
}
Stop there. A parser that “repairs” trailing commas or single quotes hides bugs that then resurface somewhere harder to debug.
The same errors in PHP and Python
Every language rejects the same inputs, with different wording.
PHP returns null from json_decode() and sets an error code. A trailing comma, an HTML page and an empty string all report the same Syntax error, which gives you little to work with. Pass JSON_THROW_ON_ERROR to get an exception instead. The json_encode flags guide covers the decode side in detail.
$data = json_decode($text, true, 512, JSON_THROW_ON_ERROR);
// JsonException: Syntax error
Python raises json.JSONDecodeError with a line and column, which is more than V8 gives you.
import json
json.loads("{'a': 1}")
# Expecting property name enclosed in double quotes: line 1 column 2 (char 1)
json.loads('<html>')
# Expecting value: line 1 column 1 (char 0)
json.loads('{"a":1}')
# Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)
“Expecting value” at char 0 is Python’s version of both “Unexpected token <” and “Unexpected end of JSON input”. Print repr(text[:200]) to see which one you have.
Error message to cause, at a glance
| Message contains | Cause | Fix |
|---|---|---|
token '<' | HTML page (404, login redirect, error) | Check status and Content-Type |
Expected property name | Single quotes or unquoted keys | Use double quotes |
Expected double-quoted property name | Trailing comma | Remove the comma |
Expected ',' or '}' | Comment or missing comma | Remove comments |
Unexpected end of JSON input | Empty or truncated body | Check status 204 and length |
token 'N' or 'u' | NaN or undefined | Serialize with a real encoder |
| blank token at position 0 | UTF-8 BOM | Strip |
| result is a string | Double-encoded | Serialize once |
Read the raw text before parsing. When the message alone is not enough, paste that text into the JSON Formatter.