Skip to content
jsonbeautifiers
English
V8 Verified by running the parser on Node v24.15.0, V8 13.6.233.17.

Unexpected token '<', "<!DOCTYPE "... is not valid JSON

Your code called JSON.parse (or res.json()) on an HTML document. The angle bracket at position 0 is the opening of <!DOCTYPE html>. The JSON is not malformed; you did not receive JSON at all. Nothing about the parser needs fixing. The request does.

You may also see this written as

  • Unexpected token < in JSON at position 0
  • Unexpected token '<', "<html><hea"... is not valid JSON
  • SyntaxError: Unexpected token < in JSON at position 0

V8 rewrote most of its JSON error messages in 2022. Older wording is still what most search results show, but no current runtime emits it.

Paste your JSON and see exactly where it breaks

Input

Nothing you paste leaves your browser. The connect-src allowlist makes that a browser guarantee rather than a promise. Check it yourself

What actually causes it

Ranked by how often each one turns out to be the answer.

  1. 01 The server returned an error page

    A 404, a 500, or a gateway error rendered as HTML. Many frameworks return HTML error pages even for API routes unless you explicitly ask for JSON. Check the status code first, not the body.

    Breaks

    const data = await fetch('/api/user').then((r) => r.json());

    Works

    const res = await fetch('/api/user', { headers: { Accept: 'application/json' } });
    if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
    const type = res.headers.get('content-type') ?? '';
    if (!type.includes('application/json')) {
      throw new Error(`Expected JSON, got ${type}: ${(await res.text()).slice(0, 200)}`);
    }
    const data = await res.json();
  2. 02 A single-page app rewrote an unknown path to index.html

    This is the most common cause in development. Vite, Create React App and most dev servers serve index.html for any path they do not recognise, so a typo in an API route or a missing proxy setting returns your own app as HTML with a 200 status. That is why checking res.ok is not enough on its own.

  3. 03 The URL resolved somewhere unexpected

    A relative path like fetch("api/user") resolves against the current route, so from /settings/profile it requests /settings/api/user. Use a leading slash or an absolute URL.

    Breaks

    fetch('api/user')

    Works

    fetch('/api/user')
  4. 04 A captive portal, VPN or proxy intercepted the request

    Hotel wifi, a corporate proxy or a Cloudflare bot challenge returns its own HTML page. This shows up as an error that only some users hit, only on some networks.

  5. 05 PHP printed a warning before the JSON

    A notice or warning echoed before the response body makes the document start with <br /> rather than {. The JSON is still in there, after the HTML. Turn display_errors off in production.

The same mistake in other runtimes

The underlying problem is identical; only the wording differs. If a colleague reports one of these, they are looking at the same thing you are.

Python Expecting value: line 1 column 1 (char 0)
C# (Newtonsoft) Unexpected character encountered while parsing value: <. Path '', line 0, position 0.
C# (System.Text.Json) '<' is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0.
Java (Jackson) Unexpected character ('<' (code 60)): expected a valid value

Questions

Why did the message change from "Unexpected token < in JSON at position 0"?
V8 rewrote its JSON error messages in 2022 to include a snippet of the offending input. Chrome 104 and later, and Node 18 and later, print the new wording. The old string is still what most search results and Stack Overflow answers show, but no current runtime emits it.
Is my JSON broken?
No. There is no JSON involved. The parser is reading an HTML document. Fix what the request returns and the error disappears without touching your JSON.
The status code is 200, so why is it HTML?
A dev server that falls back to index.html for unknown routes returns 200 with an HTML body. Check the Content-Type header as well as the status.

Fix it now

Paste the payload into the tool above, or go straight to the one built for this job.

Check a payload