Every JSON.parse Error, And What Actually Causes It

Last updated 21 September 2026 · Covers V8 (Chrome, Edge, Node), SpiderMonkey (Firefox) and JavaScriptCore (Safari)

Almost every JSON.parse failure comes down to one of eight causes, and the error message usually points at the wrong place. The position in the message is where the parser gave up, which is often several characters after the thing that actually broke. This guide lists the real messages each engine produces, what each one means, and the fix.

In a hurry? Paste the string into the JSON formatter — it reports the same parse error your code sees, but with the document formatted so you can see the context around the failure.

1. You received HTML, not JSON

Unexpected token '<', "<!DOCTYPE "... is not valid JSON Unexpected token < in JSON at position 0 (older V8) SyntaxError: JSON.parse: unexpected character at line 1 column 1 (Firefox)

This is the single most common JSON error in web development, and it is almost never a JSON problem. A < at position 0 means the response body starts with <!DOCTYPE html> or <html> — your fetch hit an error page, a login redirect, a 404, or a proxy's error template, and you called .json() on it anyway.

The fix is not in the parser. Check the response before parsing:

const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`); const type = res.headers.get("content-type") || ""; if (!type.includes("application/json")) { throw new Error(`Expected JSON, got ${type}: ${(await res.text()).slice(0, 120)}`); } return res.json();

Common culprits: a relative API URL that resolved against the wrong base and hit your SPA's index.html; an expired session returning the login page with a 200; a dev proxy that isn't running, so the dev server serves index.html for every path.

2. The input is empty or truncated

Unexpected end of JSON input SyntaxError: JSON.parse: unexpected end of data (Firefox) JSON Parse error: Unexpected EOF (Safari)

The parser reached the end of the string while still expecting something. Three realistic causes:

3. You passed an object that was already converted to a string

Unexpected token 'o', "[object Object]" is not valid JSON Unexpected token o in JSON at position 1 (older V8)

The giveaway is o at position 1. You called JSON.parse on something that was not a string, so JavaScript coerced it — String({}) is "[object Object]", and the parser reads [ as the start of an array, then chokes on o.

You are parsing something that was already parsed. Axios, for example, parses JSON responses for you: res.data is an object, and JSON.parse(res.data) is a mistake. Native fetch does not, so await res.json() is required there. Mixing the two conventions in one codebase is how this bug survives code review.

4. Single quotes instead of double quotes

Expected property name or '}' in JSON at position 1 Unexpected token ' in JSON at position 1 (older V8)

{'name': 'value'} is a valid JavaScript object literal and invalid JSON. The JSON grammar allows double quotes only, for both keys and string values. This bites hardest when someone pastes a Python dict or a JavaScript literal into a config file and expects it to parse.

Python's str(dict) output is the usual source. Use json.dumps() instead of str() and the quoting comes out right — along with True becoming true and None becoming null, which are the next two errors you would have hit.

5. A trailing comma

Expected property name or '}' in JSON at position 17 Unexpected token } in JSON at position 17 (older V8)

{"a": 1, "b": 2,} is fine in modern JavaScript and rejected by JSON. The position points at the closing brace, not at the comma that caused it — the parser only discovers the problem when it looks for the next key and finds } instead.

Hand-edited config files are the usual source, particularly after deleting the last entry from a list. If you need comments and trailing commas in configuration, you want JSON5 or JSONC, and you need a parser that supports them — JSON.parse never will.

6. Unquoted keys

Expected property name or '}' in JSON at position 1 Unexpected token n in JSON at position 1 (older V8)

{name: "value"} — again valid JavaScript, invalid JSON. Every key must be a double-quoted string. There are no exceptions for keys that happen to be valid identifiers.

7. Values JSON does not have

Unexpected token 'N', ..."value": NaN}" is not valid JSON Unexpected token u in JSON at position 10 (undefined)

JSON has exactly six value types: object, array, string, number, true/false, and null. It does not have NaN, Infinity, -Infinity, undefined, dates, functions, or comments.

This usually arrives from the serialising side. JSON.stringify handles it silently and asymmetrically, which is worth knowing:

InputJSON.stringify produces
NaN, Infinitynull
undefined as an object valuethe key is dropped entirely
undefined as an array elementnull
undefined at the top levelreturns undefined, not a string
a Datean ISO 8601 string — it will not survive a round trip as a Date
a BigIntthrows TypeError
a function or Symboldropped, like undefined

The last row in the table is the one that causes production incidents: JSON.stringify({ id: 1n }) throws rather than degrading, so a single BigInt in a payload takes down the whole serialisation.

8. A byte-order mark or invisible character

Unexpected token '', "{"name":"... is not valid JSON

A file saved as "UTF-8 with BOM" starts with the bytes EF BB BF, which decode to U+FEFF. It is invisible in every editor and it is not whitespace as far as the JSON grammar is concerned, so the parser fails at position 0 on a file that looks perfect.

Windows tooling — Notepad, some PowerShell redirections, older Visual Studio — writes BOMs by default. Strip it before parsing:

JSON.parse(text.replace(/^/, ""));

Non-breaking spaces (U+00A0) pasted from a web page or a Word document cause the same class of failure, and are equally invisible. If a document fails to parse and looks byte-perfect, check for them before anything else.

Reading the position number

Newer V8 (Chrome 114+, Node 20+) includes the offending text and a line/column, which is a large improvement:

Expected ',' or '}' after property value in JSON at position 27 (line 3 column 8)

Older engines give you only a character offset into the whole string, which is close to useless for a 40 KB document on one line. To turn an offset into something you can look at:

try { JSON.parse(text); } catch (err) { const pos = Number(/position (\d+)/.exec(err.message)?.[1] ?? -1); if (pos >= 0) { console.error(text.slice(Math.max(0, pos - 40), pos + 40)); console.error(" ".repeat(Math.min(pos, 40)) + "^"); } throw err; }

Remember that the reported position is where parsing became impossible, not where the mistake is. A missing comma on line 3 is reported at the start of line 4. Always read backwards from the position, not forwards.

Two things that are not errors, and probably should be

Duplicate keys parse cleanly. JSON.parse('{"a":1,"a":2}') returns {a: 2} — last one wins, no warning. The specification permits this, and implementations across languages disagree about which value survives, so a document with duplicate keys can mean different things to your Node service and your Python consumer.

Large integers lose precision silently. JSON numbers have no size limit; JavaScript numbers are IEEE 754 doubles. Any integer above 253−1 (9007199254740991) is rounded on parse, with no error:

JSON.parse('{"id": 9007199254740993}').id // → 9007199254740992 (the last digit changed)

Snowflake IDs from Twitter and Discord, and 64-bit database IDs, are all in this range. This is why well-behaved APIs send large identifiers as strings. If you control the consumer and not the producer, parse with a reviver or a bigint-aware parser before the value reaches a Number.

Related