SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON — almost always means one thing: your code called JSON.parse() (or res.json()) on an HTML page, not JSON. The < at position 0 is the opening of <!DOCTYPE html> or <html>. Here's exactly why it happens and how to fix it.
What the Error Looks Like
// V8 (Chrome / Node / Edge)
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
// older V8
SyntaxError: Unexpected token < in JSON at position 0
// Firefox
SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data
// Safari
SyntaxError: JSON Parse error: Unexpected identifier "<"Position 0 is the key clue: the very first byte is already wrong, so the response was never JSON to begin with.
Why It Happens: You Received HTML
A fetch or AJAX call returned an HTML document instead of the JSON you expected. The usual sources:
- A 404 / 500 error page served as HTML by your framework or web server
- A login or auth redirect to an HTML sign-in page (session expired)
- A wrong URL — hitting the app/SPA route instead of the API endpoint
- A proxy, CDN, or captive portal returning its own HTML page
Broken example
const res = await fetch('/api/user'); // server returned a 404 HTML page
const data = await res.json(); // ❌ Unexpected token '<' …
// res.json() tries to parse "<!DOCTYPE html>…" as JSONFixed example
const res = await fetch('/api/user');
// 1) Check the status before parsing
if (!res.ok) {
throw new Error(`HTTP ${res.status} ${res.statusText}`);
}
// 2) Optionally confirm it's actually JSON
const type = res.headers.get('content-type') ?? '';
if (!type.includes('application/json')) {
const text = await res.text();
throw new Error(`Expected JSON, got: ${text.slice(0, 80)}…`);
}
const data = await res.json(); // ✅ safeTwo Less-Obvious Sources Worth Knowing
- WAF-blocked or rate-limited responses. A WAF, CDN, or DDoS protection layer can intercept the call and return its own HTML challenge / block page — Cloudflare's interstitial, AWS WAF's denial page, captive-portal walls on public Wi-Fi. Status is often
403/429/503with HTML body. Inspect the response in the Network tab and check for headers likeCF-RayorX-Amz-Cf-Idto spot it. - SPA history fallback returning
index.html. Static hosts and dev servers commonly rewrite every unknown path to/index.htmlso the client-side router can take over. If your front-end accidentally fetches/api/useras a relative path and the proxy isn't configured, the server quietly returns your SPA's HTML shell with a200— and you get a confusing "200 OK but it's HTML" version of the same error.
How to Fix It — Step by Step
- Log the raw body:
console.log(await res.clone().text()). If it starts with<!DOCTYPEor<html>, you have HTML. - Check
res.ok/res.statusbefore callingres.json()— a 4xx/5xx usually returns an HTML error page. - Verify the URL in the Network tab. A relative path may resolve to your SPA's index.html instead of the API.
- Check authentication — a redirect to a login page means your token/session expired.
- Inspect
Content-Type— branch on it so non-JSON responses are handled, not blindly parsed.
Frequently Asked Questions
What does "Unexpected token < in JSON at position 0" mean?
The parser found < as the first character, which is never valid JSON. The response was HTML (a <!DOCTYPE> or <html> page), not the JSON your code expected.
Why is my fetch returning HTML instead of JSON?
Common causes: a 404/500 error page, an auth redirect to a login page, or a wrong URL that resolves to your front-end instead of the API. Check res.ok and log await res.text() to see what came back.
How do I stop it from crashing my app?
Guard before parsing: check res.ok and the Content-Type header, and wrap JSON.parse()/res.json() in try/catch so an HTML response becomes a handled error instead of an exception.
Fix It Now
If you have a response body you're not sure about, paste it into JSON Fix — it tells you immediately whether it's valid JSON or something else (like HTML). Everything runs in your browser.
- JSON Fix — validate and repair JSON in your browser
- How to Fix JSON.parse "Unexpected Token" Errors — every token variant
- Unexpected token u in JSON at position 0 — the
undefinedsibling error - Fix "[object Object] is not valid JSON" — the complete syntax-error reference