“Unexpected token < in JSON at position 0”
This is the most-searched JSON error in the world, and almost everyone starts by debugging the wrong thing. The message is not telling you that your JSON is malformed. It is telling you that what came back was never JSON at all — it was HTML.
How to read the message
Every part of the error is a clue, and together they narrow the cause down to almost one thing:
- “Unexpected token” — the parser hit a character that cannot legally start a JSON value.
- “<” — that character was an angle bracket. No JSON document of any kind begins with
<. HTML and XML both do. - “at position 0” — it failed on the very first character. Not line 40, not after a trailing comma. The response was wrong from byte one.
Put together: your code asked for JSON, received an HTML document, and handed that document to JSON.parse. The JSON is innocent. The response is the problem.
See the actual response — this is the whole fix
Stop guessing and print what really arrived. Read the body as text instead of JSON, just once, and the cause is usually obvious immediately:
const res = await fetch('/api/users');
// Read as text FIRST — .json() would throw before you can look
const body = await res.text();
console.log(res.status, res.headers.get('content-type'));
console.log(body.slice(0, 300));Nine times out of ten the console now shows something like this, and you have your answer:
404 text/html
<!DOCTYPE html>
<html><head><title>404 Not Found</title></head>
<body><h1>Not Found</h1>…The five causes, in order of likelihood
1. The URL is wrong and you got a 404 page
By far the most common cause. A typo in the path, a missing /api prefix, or a relative URL resolving against the wrong base. The server answers with its HTML 404 page, which starts with <. Check res.status — if it is 404, this is your bug.
2. The server threw a 500 and returned an HTML error page
Frameworks in development mode return richly formatted HTML stack traces. Your endpoint exists but crashed. The fix is in the server logs, not the client.
3. Your session expired and you were redirected to a login page
An authenticated API returns a 302 to /login, fetchfollows the redirect transparently, and the login page's HTML lands in your parser. The telltale sign is that it works right after signing in and breaks later. Have the API return401 with a JSON body for unauthenticated requests instead of redirecting.
4. A dev server or SPA fallback served index.html
Single-page-app dev servers are configured to return index.html for any unmatched route so client-side routing works. That catch-all happily swallows your API call too if the proxy is not set up, so every unmatched request returns your app shell. Check your proxy configuration.
5. It genuinely is XML
Some APIs return XML unless you ask otherwise. Send an explicitAccept: application/json header — or, if XML is all the service offers, run it through our XML to JSON converter.
Write the guard that prevents it forever
The root problem is that fetch does not reject on HTTP error statuses, so failures flow straight into the parser. Check the status and content type before parsing, and you get an error that names the real problem:
async function getJson(url) {
const res = await fetch(url, { headers: { Accept: 'application/json' } });
if (!res.ok) {
throw new Error(`Request failed: ${res.status} ${res.statusText}`);
}
const type = res.headers.get('content-type') ?? '';
if (!type.includes('application/json')) {
const preview = (await res.text()).slice(0, 120);
throw new Error(`Expected JSON but got ${type}. Body starts: ${preview}`);
}
return res.json();
}“Expected JSON but got text/html. Body starts: <!DOCTYPE html>” is a message you can act on at 3am. “Unexpected token < in JSON at position 0” is not.
The same error in every runtime
The wording changed in 2023, so search results may not match what you see. All of these mean exactly the same thing:
- Node 20+ / Chrome 114+:
Unexpected token '<', "<!DOCTYPE "... is not valid JSON— the newer V8 format, and a real improvement: it shows you the first characters of the response. - Older Chrome / Node:
Unexpected token < in JSON at position 0 - Firefox:
SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data - Safari:
JSON Parse error: Unrecognized token '<' - Python:
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)— covered in our Python JSONDecodeError guide.
Other tokens, other causes
When the token is not <, the character itself tells you which mistake you made:
Unexpected token o in JSON at position 1— you passed an object, not a string. It became[object Object]. Remove the redundantJSON.parse;res.json()already parsed it.Unexpected token ' in JSON— single-quoted strings. JSON requires double quotes.Unexpected token }— a trailing comma before the closing brace.Unexpected token N— a literalNaN, which JSON does not support.Unexpected token u in JSON at position 0— the value wasundefined. The variable you parsed was never assigned.Unexpected end of JSON input— an empty or truncated response. An empty string is not valid JSON; a204 No Contentresponse commonly causes this.
If the token points somewhere in the middle of a genuine JSON document, the problem really is syntax — our common JSON errors guide covers all ten, and the JSON validator underlines the exact line and column.
Checklist
- Log
res.statusand the first 300 characters ofres.text(). - Does the body start with
<!DOCTYPEor<html? Then it is HTML, not JSON. - 404 → fix the URL. 500 → read the server logs. 302 to login → fix auth handling.
- Status 200 but still HTML? Your dev-server proxy is serving the SPA fallback.
- Add the
res.okand content-type guard so the next failure names itself.
Frequently asked questions
Why does the error say “position 0”?
Position 0 is the very first character of the response. If the parser fails there, nothing about the response was ever JSON — it is not a small syntax slip further down the document.
Why is the token a “<” specifically?
Because < is the first character of <!DOCTYPE html> or <html>. Your request returned an HTML page — usually an error page, a login redirect, or a dev-server fallback.
I get “Unexpected token o in JSON at position 1” instead. Same thing?
Same family, different cause. That one means you passed an object to JSON.parse. JavaScript stringifies it to [object Object] first, and position 1 is the letter “o”. The data was already parsed — delete the extra JSON.parse call.
Does this mean my API is down?
Not necessarily. A 404, a 500, an expired session redirect, and a proxy misconfiguration all produce HTML. Log the status code alongside the body text to tell them apart.