Unexpected Token '<' in JSON at Position 0: Causes and Fixes
Getting "Unexpected token <" or "is not valid JSON" from fetch or JSON.parse? Your code received HTML, not JSON. Find the cause and fix it in minutes.
The short answer
Unexpected token '<', "<!DOCTYPE "... is not valid JSON (older browsers: Unexpected token < in JSON at position 0) means your code tried to parse an HTML page as JSON. The server sent back an error page, a login page or your app's index.html instead of the data you expected. The fix is almost never in the JSON parser; it is in the request URL, the server response or authentication. Paste the raw response into the JSON Formatter to see exactly what you received.
What the error is telling you
JSON.parse() and response.json() read text and expect the first meaningful character to be {, [, a quote, a digit, true, false or null. When the first character is <, the text is almost certainly HTML or XML, because those formats start with tags like <!DOCTYPE html> or <html>.
The wording depends on the JavaScript engine. These are the exact messages produced by JSON.parse() in Node.js 22, which uses the same V8 engine as current Chrome and Edge:
| What was parsed | Error message |
|---|---|
| An HTML page | Unexpected token '<', "<!DOCTYPE "... is not valid JSON |
| An empty response body | Unexpected end of JSON input |
| The word undefined | "undefined" is not valid JSON |
| An object turned into text | "[object Object]" is not valid JSON |
| Plain text such as OK | Unexpected token 'O', "OK" is not valid JSON |
| Text starting with an invisible BOM | Unexpected token followed by an invisible character and the start of the text |
Firefox and Safari word the same errors differently, but the cause is the same: the first character is not valid JSON.
The six usual causes
1. Wrong URL, so the server returned a 404 page
A typo in the path, a missing /api prefix or a wrong port often returns the server's HTML "Not Found" page. In single-page apps (React, Vue, Angular) served by a development server or a static host, any unknown path may return index.html with status 200, which is especially confusing because the request looks successful.
Check: open the exact request URL in a new browser tab. If you see a web page instead of raw JSON, the URL is wrong.
2. The server crashed and sent an HTML error page
Frameworks such as Express, Django, Laravel and ASP.NET return an HTML error page by default when the code throws an exception. Status codes 500, 502 and 503 from a proxy such as Nginx or a hosting platform also come with an HTML body.
Check: look at the status code in your browser's Network tab. Anything 4xx or 5xx means the problem is the server, not the JSON.
3. You are not logged in
Expired sessions often redirect API calls to a login page. fetch() follows redirects automatically, so your code receives the login page's HTML with status 200.
Check: in the Network tab, look for a 301, 302 or 303 response before the final one, or a final URL that ends in /login.
4. A proxy or CORS setup in development
If your front end runs on one port and your API on another, a missing proxy rule sends the request to the front-end server, which replies with index.html. This is the classic cause of "position 0" errors during local development.
5. The response is empty
A 204 No Content response, or an endpoint that returns nothing on success, gives Unexpected end of JSON input when you call response.json().
6. The data was converted to text the wrong way
If you see "[object Object]" is not valid JSON, something called String() on an object or put it into a template string before sending. If you see "undefined" is not valid JSON, the variable you parsed was never set, often a value read from localStorage that does not exist.
A safer fetch pattern
Instead of calling response.json() directly, check the status and content type first, then read the body as text so you can see what came back:
const res = await fetch(url)const text = await res.text()!res.ok, log res.status and the first 200 characters of text, then stop.content-type header does not include application/json, log the text and stop.JSON.parse(text).This turns a vague parse error into a clear message such as "status 404, body starts with <!DOCTYPE html>", which points straight at the real problem.
How to inspect the response quickly
The formatter runs the same JSON.parse() in your browser. If the text is valid, you see it indented with 2 or 4 spaces and a "Valid JSON" badge. If not, the browser's error message appears above the output, so you can compare it with what your app reported. Nothing you paste is sent to a server, but avoid pasting responses that contain passwords or tokens on a shared computer.
Errors that look similar but have different causes
"Expected property name or '}' in JSON at position 1"
This is not HTML. The text starts with { but the key is not in double quotes, for example {'a': 1} with single quotes. JSON requires double quotes around every key and string.
"Expected double-quoted property name in JSON at position 7"
Usually a trailing comma: {"a": 1,}. The parser sees the comma and expects another key. Remove the last comma.
Unexpected token 'N'
Often NaN, which JavaScript has but JSON does not allow. JSON.stringify() turns NaN into null, so this usually comes from a hand-written or non-JavaScript source.
The BOM case
Some Windows tools save files with an invisible byte order mark at the start. It looks like a normal { in an editor but fails to parse. Re-save the file as "UTF-8" without BOM, or strip the first character in code with text.replace(/^\uFEFF/, '').
Double-encoded JSON
If parsing succeeds but gives you a string such as {"a":1} instead of an object, the server encoded the data twice. Fix the server, or call JSON.parse() a second time as a temporary workaround.
Checklist
- First character is
<? You received HTML: check the URL, status code and login state. - Empty body? Handle 204 responses before calling
.json(). [object Object]orundefined? Fix how the data is created, not how it is parsed.- Valid-looking text still fails? Look for single quotes, trailing commas or a BOM.
For the basic JSON syntax rules, see how to format and validate JSON. If the response is XML rather than HTML, the XML Formatter will show whether it is well formed.
Frequently asked questions
What does Unexpected token < in JSON at position 0 mean?
The text being parsed starts with <, which means it is HTML or XML, usually an error page, a login page or index.html, not the JSON you expected.
Why does fetch return HTML instead of JSON?
Common causes are a wrong URL returning a 404 page, a server error page, an expired session redirecting to login, or a missing development proxy.
What causes Unexpected end of JSON input?
The response body was empty, for example a 204 No Content reply, and the code still called response.json() or JSON.parse().
How do I see what the server actually returned?
Open the browser Network tab, click the failing request, and check the Status and Response. Read the body with response.text() before parsing.
Put this guide into practice with our free online tool — no signup required.
Open tool