← All articles

Unexpected Token < in JSON at Position 0: You Got HTML

The "Unexpected token <" error means JSON.parse received an HTML page (a 404, login redirect, or wrong URL), not JSON. Here's why, with broken/fixed fetch examples.

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 JSON

Fixed 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();           // ✅ safe

Two 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 / 503 with HTML body. Inspect the response in the Network tab and check for headers like CF-Ray or X-Amz-Cf-Id to spot it.
  • SPA history fallback returning index.html. Static hosts and dev servers commonly rewrite every unknown path to /index.html so the client-side router can take over. If your front-end accidentally fetches /api/user as a relative path and the proxy isn't configured, the server quietly returns your SPA's HTML shell with a 200 — and you get a confusing "200 OK but it's HTML" version of the same error.

How to Fix It — Step by Step

  1. Log the raw body:console.log(await res.clone().text()). If it starts with <!DOCTYPE or <html>, you have HTML.
  2. Check res.ok / res.status before calling res.json() — a 4xx/5xx usually returns an HTML error page.
  3. Verify the URL in the Network tab. A relative path may resolve to your SPA's index.html instead of the API.
  4. Check authentication — a redirect to a login page means your token/session expired.
  5. 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.