Every JSON.parse Error, And What Actually Causes It
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
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:
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
The parser reached the end of the string while still expecting something. Three realistic causes:
- The body was empty. A
204 No Contentor an empty200gives you"", andJSON.parse("")throws. Guard withtext ? JSON.parse(text) : null. - The response was cut off. A timeout, a killed connection or a truncated log line leaves you with valid-looking JSON that simply stops. Check the string's length against the
Content-Lengthheader. - An unclosed bracket or quote.
{"a": 1and{"a": "unterminated}both produce this, because the parser is still waiting for the closing token when the input ends.
3. You passed an object that was already converted to a string
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
{'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
{"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
{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
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:
| Input | JSON.stringify produces |
|---|---|
NaN, Infinity | null |
undefined as an object value | the key is dropped entirely |
undefined as an array element | null |
undefined at the top level | returns undefined, not a string |
a Date | an ISO 8601 string — it will not survive a round trip as a Date |
a BigInt | throws TypeError |
a function or Symbol | dropped, 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
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:
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:
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:
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:
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
- JSON formatter and validator — reproduces the exact parse error with context
- JSONPath tester — query a document once it parses
- JSONPath cheatsheet