Unexpected token < in JSON at position 0: What It Means and How to Fix It
If you have hit Unexpected token < in JSON at position 0, your code tried to parse JSON and got back something that starts with a less-than sign. Nine times out of ten, that character is the opening of an HTML page. The first bytes are <!DOCTYPE html> or <html>. Your request expected JSON, and the server returned an error page, a login redirect, or a plain 404 instead.
The message is blunt because the parser reads the very first character, sees a <, and stops. “Position 0” is the giveaway: the response was never JSON in the first place. This guide explains what actually triggers the error, how to confirm the cause in under a minute, and the fixes that hold up in production.
Why a < means you got HTML, not JSON
Valid JSON only ever starts with {, [, a quote, a digit, or the letters in true, false, and null. It never starts with <. So when the parser complains about a < at position 0, the body it received was markup, not data. The usual sources:
- A 404 or 500 error page. The route was wrong or the server threw an exception, and the framework returned an HTML error page that your code still tried to parse as JSON.
- A login or session redirect. The session expired, so the server returned the HTML login screen instead of the JSON the endpoint normally sends.
- A proxy or CDN interstitial. A Cloudflare challenge, a maintenance notice, or a rate-limit page. All of them are HTML.
- A wrong URL. You called the API path but hit your single-page app’s
index.htmlfallback, which is HTML by design.
Every one of these returns a web page. The JSON parser is simply the first place that page causes a visible failure.
Confirm the cause in under a minute
Do not guess. Look at the raw response before you parse it. Read it as text first, not as JSON:
const res = await fetch('/api/users');
const text = await res.text(); // read as text, not res.json()
console.log(res.status, res.headers.get('content-type'));
console.log(text.slice(0, 120)); // see what actually came back
If the text starts with <!DOCTYPE html> or <html>, the endpoint returned a page, not data. The content-type header will usually say text/html instead of application/json, which confirms it. In the browser, the Network tab shows the same story: open the request, click Response, and read the first line.
Fix the request, not the parser
Once you know the body is HTML, the parse error is only a symptom. Fix what produced the wrong response.
Check the status code before parsing. A failed request should never reach JSON.parse:
const res = await fetch('/api/users');
if (!res.ok) {
throw new Error(`Request failed with ${res.status}`);
}
const data = await res.json(); // only parse a confirmed success
Check the URL and base path. Make sure you are hitting the API and not the front-end router’s catch-all route that serves HTML for unknown paths.
Handle authentication. If the body is a login page, refresh the token or re-authenticate before retrying the request.
Read the content type. Only call res.json() when the response advertises application/json. Otherwise, surface the text so the real error is visible instead of hidden behind a parser exception.
The other “Unexpected token” variants
When the position is something other than 0, the response probably is JSON, just malformed. These are the variants developers hit most, with the cause and the fix for each. The quickest way to locate any of them is to paste the payload into the JSON parser, which reports the exact line and character where parsing breaks.
| Error message | Cause | Fix |
|---|---|---|
Unexpected token < … position 0 |
HTML returned instead of JSON | Fix the request; guard on the status code |
Unexpected token ' |
Single quotes instead of double | JSON requires double quotes on keys and strings |
Unexpected token } |
A trailing comma before a closing brace or bracket | Remove the trailing comma |
Unexpected token o in JSON |
You passed an object, not a string | Do not parse a value that is already parsed |
| Unexpected end of JSON input | A truncated or empty body | Confirm the full response arrived |
Unexpected token / |
A // or /* */ comment |
JSON has no comments; strip them |
Two of these deserve a note. Unexpected token o means you called the parser on a value that was already a JavaScript object: it gets converted to the string [object Object], and the parser chokes on the first o. And a hidden byte-order mark or stray whitespace can also break position 0, which is hard to spot by eye but obvious once you run the text through the JSON formatter or the JSON viewer.
How other languages report the same error
The wording is JavaScript’s, but every language has an equivalent:
- JavaScript:
JSON.parse()throws aSyntaxError. Wrap it intry/catchso one bad response cannot crash the caller. - Python:
json.loads()raisesJSONDecodeError, with line and column numbers that point straight at the problem. - PHP:
json_decode()returns null and setsjson_last_error(); check that rather than trusting the return value.
The habit is the same everywhere: parse defensively, and inspect the raw text whenever parsing fails instead of assuming the data was valid.
A repeatable debugging routine
When this error appears, run the same three steps every time:
- Read the response as text and log the first 100 characters. HTML announces itself on the first line.
- Check the status code and content type. A non-success status or a
text/htmltype means the body was never meant to be JSON. - If it really is JSON, validate it. Paste it into the JSON parser to jump to the exact line and column, then fix the quote, comma, or bracket it flags. To make an escaped or minified payload readable first, run it through the JSON formatter or the JSON pretty-print tool, or clean up stray characters with JSON escape.
Guard your requests and inspect what you actually received, and this error stops being a mystery. It is the parser telling you, correctly, that it was handed a web page.
Frequently asked questions
Does “position 0” always mean HTML?
Almost always. Position 0 means the first byte was already invalid JSON, and the most common invalid first byte is the < that opens an HTML document. A leading byte-order mark or stray whitespace can occasionally cause it too.
Why does my code get HTML when the API works fine in the browser?
Usually authentication. Your browser holds a valid session cookie; your code does not, so the server redirects to an HTML login page instead of returning JSON.
How do I stop it from crashing my app?
Check the response status before parsing, and wrap the parse call in try/catch. Never parse a response you have not confirmed is a successful JSON payload.
The error points at a later position, not 0. Is it still HTML?
No. A non-zero position means the body is JSON but malformed somewhere inside it. Run it through the JSON parser to find the exact character.
Leave a Reply