Unexpected token '<', "<!DOCTYPE "... is not valid JSON
Your code called JSON.parse (or res.json()) on an HTML document. The angle bracket at position 0 is the opening of <!DOCTYPE html>. The JSON is not malformed; you did not receive JSON at all. Nothing about the parser needs fixing. The request does.
You may also see this written as
- Unexpected token < in JSON at position 0
- Unexpected token '<', "<html><hea"... is not valid JSON
- SyntaxError: Unexpected token < in JSON at position 0
V8 rewrote most of its JSON error messages in 2022. Older wording is still what most search results show, but no current runtime emits it.
Paste your JSON and see exactly where it breaks
Nothing you paste leaves your browser. The connect-src allowlist makes that a browser guarantee rather than a promise. Check it yourself
What actually causes it
Ranked by how often each one turns out to be the answer.
-
01 The server returned an error page
A 404, a 500, or a gateway error rendered as HTML. Many frameworks return HTML error pages even for API routes unless you explicitly ask for JSON. Check the status code first, not the body.
Breaks
const data = await fetch('/api/user').then((r) => r.json());Works
const res = await fetch('/api/user', { headers: { Accept: 'application/json' } }); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); const type = res.headers.get('content-type') ?? ''; if (!type.includes('application/json')) { throw new Error(`Expected JSON, got ${type}: ${(await res.text()).slice(0, 200)}`); } const data = await res.json(); -
02 A single-page app rewrote an unknown path to index.html
This is the most common cause in development. Vite, Create React App and most dev servers serve index.html for any path they do not recognise, so a typo in an API route or a missing proxy setting returns your own app as HTML with a 200 status. That is why checking res.ok is not enough on its own.
-
03 The URL resolved somewhere unexpected
A relative path like fetch("api/user") resolves against the current route, so from /settings/profile it requests /settings/api/user. Use a leading slash or an absolute URL.
Breaks
fetch('api/user')Works
fetch('/api/user') -
04 A captive portal, VPN or proxy intercepted the request
Hotel wifi, a corporate proxy or a Cloudflare bot challenge returns its own HTML page. This shows up as an error that only some users hit, only on some networks.
-
05 PHP printed a warning before the JSON
A notice or warning echoed before the response body makes the document start with <br /> rather than {. The JSON is still in there, after the HTML. Turn display_errors off in production.
The same mistake in other runtimes
The underlying problem is identical; only the wording differs. If a colleague reports one of these, they are looking at the same thing you are.
| Python | Expecting value: line 1 column 1 (char 0) |
|---|---|
| C# (Newtonsoft) | Unexpected character encountered while parsing value: <. Path '', line 0, position 0. |
| C# (System.Text.Json) | '<' is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0. |
| Java (Jackson) | Unexpected character ('<' (code 60)): expected a valid value |
Questions
- Why did the message change from "Unexpected token < in JSON at position 0"?
- V8 rewrote its JSON error messages in 2022 to include a snippet of the offending input. Chrome 104 and later, and Node 18 and later, print the new wording. The old string is still what most search results and Stack Overflow answers show, but no current runtime emits it.
- Is my JSON broken?
- No. There is no JSON involved. The parser is reading an HTML document. Fix what the request returns and the error disappears without touching your JSON.
- The status code is 200, so why is it HTML?
- A dev server that falls back to index.html for unknown routes returns 200 with an HTML body. Check the Content-Type header as well as the status.
Fix it now
Paste the payload into the tool above, or go straight to the one built for this job.
Check a payload