Unexpected token '<', "<!DOCTYPE "... is not valid JSON
Your code asked for JSON and got an HTML page instead. <!DOCTYPE html> is how an HTML page starts, and no JSON document can start with <. Nothing is wrong with your parsing code. The problem is the response, so the fix is to find out why the server sent a page.
How the message appears
Chrome and Node first, then the older V8 wording, then Firefox, then Safari. The last one is what Safari says from response.json(), and it does not mention JSON at all. Every one of these is the same failure: the first character was <.
The fix
Check the response before you parse it. response.ok is false for every error status, and the Content-Type header should include application/json. When either check fails, read the body with response.text() and log it, so you can see the page that came back. Then fix whatever it points to: the URL, the proxy or the login.
<!DOCTYPE html>
<html>
<head><title>404 Not Found</title></head>
<body>Not Found</body>
</html>{
"id": 42,
"name": "Ada"
}The example travels in the URL fragment, which browsers never send to a server.
What causes it
The URL is wrong, so you got the 404 page
A typo in the path, a missing /api prefix, or a trailing slash the server treats differently. Most frameworks answer an unknown URL with an HTML error page, not a JSON one. Open the exact URL from the Network tab in a new browser tab and you will see the page your code tried to parse.
A single-page app answered with index.html
Dev servers like Vite, and static hosts with a catch-all rewrite, answer every unknown path with index.html so client-side routing works. Ask for /api/users when no API is running there and you get the app shell with a 200. The status looks fine and the body is HTML.
The request needed a login
An expired session or a missing token often gets a redirect to the login page rather than a 401. fetch follows redirects by default, so your code receives the login form.
The server failed and sent its error page
A 500 from your framework, a 502 from a proxy or a 503 from a load balancer usually comes with an HTML body. fetch does not reject on an error status, so response.json() runs on that page and throws this instead of telling you about the 500.
Worth knowing
- A body can only be read once. If you want the text for your logs when parsing fails, call
response.text()first and thenJSON.parse(text)inside atry, rather than callingresponse.json(). - Safari words this differently.
JSON.parsesaysUnrecognized token '<', andresponse.json()saysThe string did not match the expected pattern., which gives no hint that JSON is involved. It is the same problem. - The same failure in other languages: Python says
Expecting value: line 1 column 1 (char 0)and Go saysinvalid character '<' looking for beginning of value. Both are in this reference.
Written by Vishnu Shankar.