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.

SituationOld messageNew message
HTML pageUnexpected token < in JSON at position 0Unexpected token '<', "<!DOCTYPE "... is not valid JSON
Single quotesUnexpected token ' in JSON at position 1Expected property name or '}' in JSON at position 1
Trailing commaUnexpected token } in JSON at position 8Expected double-quoted property name in JSON at position 8
Empty stringUnexpected end of JSON inputUnexpected 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. fetch follows 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.html for 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/html means a page, and application/json means 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 containsCauseFix
token '<'HTML page (404, login redirect, error)Check status and Content-Type
Expected property nameSingle quotes or unquoted keysUse double quotes
Expected double-quoted property nameTrailing commaRemove the comma
Expected ',' or '}'Comment or missing commaRemove comments
Unexpected end of JSON inputEmpty or truncated bodyCheck status 204 and length
token 'N' or 'u'NaN or undefinedSerialize with a real encoder
blank token at position 0UTF-8 BOMStrip 
result is a stringDouble-encodedSerialize once

Read the raw text before parsing. When the message alone is not enough, paste that text into the JSON Formatter.

Tools used in this guide

Copied to clipboard!