How to Fix JSON Parse Errors — Unexpected token '<' and More
JSON errors arrive as one terse line, but the causes behind them are a short list: the server sent HTML instead of JSON, there was nothing to parse, or the JSON was written by hand like a JavaScript object. This guide quotes the exact messages Chrome, Node.js and Python print for each case, and what to check for every one of them.
Paste JSON to pretty-print it. On a syntax error it shows the message and the line and column, and highlights the line. Everything runs in your browser — nothing is uploaded.
How to read the message
The same broken JSON produces different wording depending on what reads it. Here is what each environment actually printed for {"name": "kim", "age": 20,}, which has a trailing comma.
| Environment | Message |
|---|---|
| Chrome, Edge, Node.js | Expected double-quoted property name in JSON at position 26 (line 1 column 27) |
Python json | Expecting property name enclosed in double quotes: line 1 column 27 (char 26) |
| Older Chrome / Node.js | Unexpected token } in JSON at position 26 |
Two rules for the numbers. position (and Python's char) counts characters from 0; line and column count from 1, like your editor. So position 26 and column 27 point at the same character.
The reported position is where the parser could no longer continue, not where you made the mistake. Start there and read backwards.
Unexpected token '<' — the server sent HTML, not JSON
This is the error you are most likely to see after calling an API with fetch and then res.json(). The wording differs by browser, the meaning does not.
Chrome/Edge : SyntaxError: Failed to execute 'json' on 'Response': Unexpected token '<', "<!DOCTYPE "... is not valid JSON
Firefox : SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data
Safari : SyntaxError: JSON Parse error: Unrecognized token '<'The < is the first character of <!DOCTYPE html>. An HTML document arrived where JSON was expected, so staring at the code that builds your JSON will not help. The usual reasons:
- A typo or a changed API path — the server answers with its 404 page
- A server-side crash — the 500 error page is HTML
- An expired session — the request is redirected to a login page
- A dev server without a proxy — React or Vue dev servers return
index.htmlfor/api/... - SPA fallback on static hosting — every unknown path serves
index.html
How to confirm it
- 1Open DevTools (F12) → Network → click the request → Response. If you see HTML, you have your answer.
- 2Check the Status column for 404, 500 or 302.
- 3Look at
Content-Typeunder Headers.text/htmlmeans the server never sent JSON.
Guard against it in code
const res = await fetch("/api/users");
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const type = res.headers.get("content-type") ?? "";
if (!type.includes("application/json")) {
const body = await res.text();
throw new Error(`Expected JSON, got: ${body.slice(0, 80)}`);
}
const data = await res.json();Now the console says HTTP 404 instead of Unexpected token '<'. If the status is 200 and you still get HTML, suspect a login redirect or a missing proxy rule.
Unexpected end of JSON input — nothing to parse
Chrome/Node : SyntaxError: Unexpected end of JSON input
Python : json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)The input ended before the parser could read a single value — you parsed an empty string. Note that Python reports an empty string and an HTML page identically, as Expecting value: line 1 column 1 (char 0), so in Python the fastest check is to print what you actually received.
- The server replied
204 No Contentand the code still calledres.json() - The server sent an empty body on an error (check the status code too)
- The file is zero bytes, or a program died while writing it
const res = await fetch("/api/items/3", { method: "DELETE" });
const text = await res.text();
const data = text ? JSON.parse(text) : null; // empty body -> nullTruncated JSON reads differently. If the input stops midway, like {"name": "kim", "tags": ["a", "b", Chrome and Node.js report Expected ',' or ']' after array element in JSON at position 33 — the next required symbol never came. When the position equals the very end of the input, suspect truncation: a log line copied without its tail, or a response cut off by a size limit.
JavaScript syntax is not JSON — the common slip-ups
JSON grew out of JavaScript object notation but is far stricter. Hand-edit a JSON file with object-literal habits and these are the errors you get.
| Mistake | Example | Chrome / Node.js | Python |
|---|---|---|---|
| Trailing comma in object | {"a": 1,} | Expected double-quoted property name | Expecting property name enclosed in double quotes |
| Trailing comma in array | [1, 2, 3,] | Unexpected token ']' | Expecting value |
| Single quotes | {'name': 'kim'} | Expected property name or '}' | Expecting property name enclosed in double quotes |
| Unquoted key | {name: "kim"} | Expected property name or '}' | Expecting property name enclosed in double quotes |
| Missing comma | {"a": 1 "b": 2} | Expected ',' or '}' after property value | Expecting ',' delimiter |
| Comment | {"a": 1 // note} | Expected ',' or '}' after property value | Expecting ',' delimiter |
Notice that the message rarely names the mistake. A comment does not produce "comments are not allowed"; it produces "expected ',' or '}'". Trust the position more than the wording, and look around it for one of the mistakes in this table.
tsconfig.json and VS Code's settings.json are JSONC, which allows comments. Carry that habit into package.json or an API payload and parsing fails.
Values JSON does not have — NaN, True, None, 007
JSON allows exactly six kinds of value: strings, numbers, true/false, null, arrays and objects. Anything borrowed from another language's syntax fails.
| Value | Chrome / Node.js | Fix |
|---|---|---|
NaN, Infinity | Unexpected token 'N' / 'I' | Store null or a string instead |
True, None (Python repr) | Unexpected token 'T' / 'N' | Build it with json.dumps(), which writes true and null |
undefined | Unexpected token 'u' | Use null. JSON.stringify drops undefined properties entirely |
007 (leading zero) | Unexpected number in JSON at position 7 | The number 7, or the string "007" |
0x1F (hex) | Expected ',' or '}' after property value | Store the decimal 31 |
Unexpected token 'N' shows up for both NaN and None. The quoted fragment that follows ("{"score": NaN}" is not valid JSON) is the original text around the problem, so read it to tell which one you have.
When Python-made JSON breaks in JavaScript
Python's json module writes NaN and Infinity by default and happily reads them back. Everything works between Python programs, then a browser or Node.js reads the file and fails.
import json
json.dumps({"score": float("nan")})
# '{"score": NaN}' <- not standard JSON
json.dumps({"score": float("nan")}, allow_nan=False)
# ValueError: Out of range float values are not JSON compliantWith allow_nan=False the error happens at write time, before bad data leaves your program. A related slip: strings made with str(data) or print(data) look like {'ok': True, 'v': None} — single quotes, True, None — and are not JSON. Use json.dumps() whenever you need JSON.
Errors inside strings — newlines, backslashes, BOM
Bad control character in string literal
Chrome/Node : Bad control character in string literal in JSON at position 14 (line 1 column 15)
Python : Invalid control character at: line 1 column 15 (char 14)A real line break or tab sits inside a quoted string. In JSON a newline inside a string must be written as the two characters \n. Build the JSON with JSON.stringify() or json.dumps() rather than concatenating strings, and this is handled for you.
Bad escaped character — Windows paths
Chrome/Node : Bad escaped character in JSON at position 13 (line 1 column 14)
Python : Invalid \escape: line 1 column 13 (char 12)In {"path": "C:\Users\kim"} each single backslash starts an escape sequence, and \U is not a valid one. Double the backslashes (C:\\Users\\kim) or use forward slashes (C:/Users/kim) — Python's and Node.js's file functions accept forward slashes on Windows. Also note the one-character difference: Python points at the backslash, Chrome and Node.js at the U after it.
The invisible BOM
Chrome/Node : Unexpected token '', "{"a": 1}" is not valid JSON
Python : Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)If a file looks perfect but fails on its very first character, suspect a BOM (byte order mark), an invisible marker at the start of the file. In Chrome and Node.js it shows up as Unexpected token '' with seemingly nothing between the quotes. On Windows, PowerShell 5.1 is a frequent source: Set-Content -Encoding UTF8 writes a BOM, and > or Out-File write UTF-16, which Node.js reports as Unexpected token '�'.
- Python: open the file with
encoding="utf-8-sig"and the BOM is skipped. - Node.js:
JSON.parse(text.replace(/^\uFEFF/, ""))strips a leading BOM. - VS Code: click the encoding in the status bar (
UTF-8 with BOM), choose Save with Encoding →UTF-8. - PowerShell 7 writes UTF-8 without a BOM by default, so it avoids the problem.
"undefined" is not valid JSON — you passed something that is not a string
JSON.parse expects a string. Anything else is converted to a string first, which is why the message quotes that value's string form instead of your JSON.
| Message | What was passed | Typical cause |
|---|---|---|
"undefined" is not valid JSON | undefined, or the string "undefined" | Reading back a value saved with localStorage.setItem("user", undefined) |
"[object Object]" is not valid JSON | Something already an object | Parsing axios's response.data or a res.json() result a second time |
Unexpected non-whitespace character after JSON | Two JSON values back to back | Parsing a whole JSON Lines (.jsonl) file in one go |
// Save: store null rather than undefined
localStorage.setItem("user", JSON.stringify(user ?? null));
// Load: getItem returns null if nothing was saved
const raw = localStorage.getItem("user");
const saved = raw ? JSON.parse(raw) : null;
// JSON Lines: parse line by line
const rows = text.split("\n").filter(Boolean).map((line) => JSON.parse(line));localStorage converts every value to a string, so storing undefined leaves the nine-character string "undefined" behind. If a bad value is already stored, delete that key in DevTools → Application. And axios parses JSON responses for you, so response.data never needs another JSON.parse.
A fast checklist
- 1See
<in the message? Check the response, not the JSON (Network tab). - 2
end of JSON input? Check whether the input is empty or cut off. - 3Got a
positionorline/column? Go there and look at the character just before it. - 4No position, or a huge file? Paste it into a JSON formatter to see the failing line. On the command line,
python -m json.tool file.jsonprints a location such asline 4 column 1. - 5Generating JSON in code? Stop concatenating strings and use
JSON.stringify()orjson.dumps(). Nearly every syntax error in this guide comes from hand-built JSON.
The JSON formatter checks your input inside the browser and never sends it to a server, so you can locate errors in data you would not want to paste into an online service.
FAQ
Q. Is there any way to put comments in JSON?
Not in standard JSON. A common workaround is a key such as "_comment": "...". For config files, check whether the program reading them supports JSONC or JSON5. Neither JSON.parse nor Python's json module accepts comments.
Q. The position is one less than my editor's column number.
Position (Python's char) counts from 0 and editor columns count from 1. Current Chrome and Node.js also print (line 1 column 27), which you can feed straight into Go to Line (Ctrl+G in VS Code).
Q. Python reads the file, but JavaScript throws.
It almost certainly contains NaN or Infinity. Python's json module accepts them by default; standard JSON and JavaScript do not. Catch it on the writing side with allow_nan=False, and store None for missing values.
Q. Python writes non-English text as `\ud55c\uae00`. Is it corrupted?
No. json.dumps escapes every non-ASCII character as \u by default, and it decodes back to the original text. If you want the file itself to be readable, use json.dumps(data, ensure_ascii=False).
Q. What changes if I use JSON.parse(await res.text()) instead of res.json()?
The parsed result is the same. Getting the text first means that when parsing fails you can log what actually arrived, which makes the cause far easier to find. In production, logging the first 100 characters or so on failure is a common pattern.
Paste JSON to pretty-print it. On a syntax error it shows the message and the line and column, and highlights the line. Everything runs in your browser — nothing is uploaded.