Table of Contents
What the error actually means
Unexpected token < in JSON at position 0 is JavaScript’s way of saying: “I tried to parse this as JSON, and the very first character was a <, which is not valid JSON.”
JSON can only start with {, [, a quote, a digit, or one of the literals true, false, null. It can never start with <. So a < at position 0 means you did not receive JSON — you received HTML (or XML). The parser never got to your data because it choked on the first byte.
Here are the five causes, in order of how often they show up.
1. The server returned an HTML error page
This is the big one. Instead of your API response, you got a generic page:
<!DOCTYPE html>
<html>
<head><title>404 Not Found</title></head>
Common triggers:
- The endpoint URL is wrong (typo, missing
/api, wrong environment). - The request was redirected to a login page.
- A proxy or gateway returned a 502/503 HTML page.
- CORS preflight failed and the browser swallowed the real error.
Fix: stop parsing and look at the raw response.
const res = await fetch('/api/user');
const text = await res.text(); // do NOT res.json() yet
console.log(res.status, res.headers.get('content-type'));
console.log(text.slice(0, 200)); // see what you actually got
If content-type is text/html and the body starts with <, fix the request (URL, auth, headers) before parsing.
2. A BOM or hidden character at the start
A UTF-8 BOM () or a stray space before the first { shifts everything and can surface as a parse error. This happens most often when a .json file is saved with a BOM or concatenated from a shell command.
Fix: trim and strip the BOM before parsing:
const clean = text.replace(/^/, '').trim();
JSON.parse(clean);
3. You got JSONP, not JSON
Some legacy endpoints wrap JSON in a callback: handleResponse({ "ok": true }). That is valid JavaScript but not valid JSON, so JSON.parse fails on the first ( — or, if the wrapper starts with a < from an error page, on the <.
Fix: call the JSON endpoint (usually ?format=json instead of ?callback=...), or strip the wrapper if JSONP is unavoidable.
4. The response is double-encoded
Sometimes the value is a JSON string containing JSON: "{\"ok\":true}". If you parse once you get a string; parse it again and you get the object. But if something interpolated it wrong, you may see a literal < from a serialized error.
Fix: confirm the top-level type before parsing, and only parse once:
const data = JSON.parse(text);
const value = typeof data === 'string' ? JSON.parse(data) : data;
5. Compression or encoding was not decoded
If the server sends Content-Encoding: gzip (or br) but your client did not decompress, you are parsing compressed bytes — which look like garbage and can start with any byte, including one that renders as < in a log.
Fix: let fetch handle decompression automatically (it does by default), or decompress explicitly in Node with zlib.
A 30-second debugging workflow
await res.text()and printstatus+content-type.- If it is
text/html, you have a request/route problem, not a JSON problem. - If it is JSON but still fails, strip the BOM and trim, then parse.
- When in doubt, paste the raw text into a formatter that shows the exact failing position.
Validate before you ship
You do not have to find these by hand. The JSON Formatter on Toolbox validates and pretty-prints a pasted string and points at the exact character that breaks parsing.
Related tools
- JSON Formatter on Toolbox
- JSON Formatter on Toolhub
- JSON Formatter on DevTools
- JSON Validator on DevTools
Next steps
- Avoid the sibling trap in YAML to JSON Gotchas.
- Base64 is often the encoding hiding inside a broken payload — see Base64 Images in CSS.
Frequently asked questions
Why does the error say position 0?
Position 0 is the first character of the response body. A `<` there means the response starts with HTML (such as `<!DOCTYPE` or `<html>`), so the server never returned JSON.
Is the JSON I sent the problem?
Usually not. The error is about the response you received, not the request you sent. Log the raw response body to see what actually came back.
Why does it work in the browser but fail in my code?
Browsers and some fetch wrappers hide the real status. Check the HTTP status code and Content-Type — a 403, 404, or login redirect returns HTML, which then fails to parse as JSON.
Guide Info
- Category
- How-to
- Difficulty
- Intermediate
- Reading Time
- 5 min
- Tags
- #json #parse-error #debugging #api
Next guide
YAML to JSON Gotchas: When Types and Indentation Break
YAML looks like JSON's friendly cousin until a boolean, a date, or a leading zero gets silently coerced. Here are the conversions that bite people.