Unexpected token '<': your code got an HTML page, not JSON
By Santhi · Messages captured October 9, 2026 on 11 runtimes · How we test
The parser read a < as the first character. JSON never starts with <, but every HTML page does. Your request reached something that answered with a web page, and the code passed that page to JSON.parse or response.json().
What each runtime prints
We ran this input through JSON.parse in each browser and Node version, and through json.loads in Python. These are the messages exactly as printed.
<!DOCTYPE html><html><body>Not Found</body></html>
| Runtime | Exact message |
|---|---|
| Chrome 151Node 24Node 22Node 20 | Unexpected token '<', "<!DOCTYPE "... is not valid JSON |
| Firefox 153 | JSON.parse: unexpected character at line 1 column 1 of the JSON data |
| Safari 26 (WebKit)Bun | JSON Parse error: Unrecognized token '<' |
| Node 18 | Unexpected token < in JSON at position 0 |
| Python 3.13Python 3.12Python 3.10 | JSONDecodeError: Expecting value: line 1 column 1 (char 0) |
- Chrome and Node 20+ quote the start of what they got, so "<!DOCTYPE " or "<html><hea" in the message is the start of the page itself.
- Python says "Expecting value: line 1 column 1 (char 0)". It says exactly the same for an empty body, so in Python you cannot tell the two apart from the message. Print the first 200 characters of the body.
- Node 18 and older say "Unexpected token < in JSON at position 0", without quotes around the <.
Paste the response body (DevTools, Network tab, Response). It is checked in your browser and not sent anywhere.
Common causes
- 1
A 404 page: the URL is wrong
A typo in the path, a missing /api or /v1 prefix, or a base URL that points at the website instead of the API. The server answers with its normal "Not Found" page.
- 2
A login page: the session expired
The token or cookie expired, or the request was sent without it, so the server redirected to sign-in. fetch follows redirects without telling you: res.status is 200 and res.redirected is true.
- 3
Your own app's index.html: the dev proxy is missing
In a Vite, Create React App or Angular dev server, a request to /api/... that the proxy does not handle falls through to the single-page-app fallback, which serves index.html for every unknown path. The same happens on a static host with a "rewrite everything to index.html" rule.
- 4
A proxy or CDN error page: the server behind it is down
nginx, Cloudflare or a load balancer answers with its own 502, 503 or 504 page when the app behind it is down, restarting or too slow.
- 5
A server error page: the app crashed
Frameworks in debug mode return an HTML stack trace for a 500. The fix is on the server; its logs have the details.
How to fix it
Check the status and the Content-Type before parsing, and put the start of the body in the error. Then the error says which of the five pages you got.
JavaScript
const res = await fetch(url);
const text = await res.text();
const type = res.headers.get('content-type') ?? '';
if (!res.ok || !type.includes('application/json')) {
throw new Error(`${res.status} from ${res.url}: ${text.slice(0, 200)}`);
}
const data = JSON.parse(text);Python
r = requests.get(url, timeout=10)
if not r.ok or "application/json" not in r.headers.get("Content-Type", ""):
raise RuntimeError(f"{r.status_code} from {r.url}: {r.text[:200]}")
data = r.json()- res.url is the address after redirects. If it ends in /login or /signin, the session is the problem, not the API.
- In the browser, open DevTools, go to the Network tab, click the request and look at Response. You will see the page.