json

FormatValidateConvert

Unexpected token < in JSON at position 0

Your code asked for JSON and got a web page. The < at position 0 is the first character of <!DOCTYPE html> or <html>, and no JSON document can begin with it. The fault is almost never in the parser or in your parsing code; it is in the request that fetched the text.

Input

<!DOCTYPE html>
<html><body>502 Bad Gateway</body></html>

Do

  1. Paste the input into the validator.
  2. Press Repair in the editor band.

Result

Nothing here could be repaired automatically.

Try it in the validator →

What each runtime prints for this input
RuntimeMessage
Chrome 153 · Node.js 26Unexpected token '<', "<!DOCTYPE "... is not valid JSON
Node.js 18 (older V8)Unexpected token < in JSON at position 0
Firefox 156JSON.parse: unexpected character at line 1 column 1 of the JSON data
Safari 26JSON Parse error: Unrecognized token '<'
Python 3.14 jsonExpecting value: line 1 column 1 (char 0)
This siteUnexpected "<"line 1, column 1

Recorded from each runtime; the wording is in Firefox’s, Safari’s and Python’s sources.

Two spellings of one message

Search results mix two wordings because V8, the engine inside Chrome, Edge and Node.js, rewrote its JSON messages between Node.js 18 and Node.js 20. Node.js 18 still prints the older form, Unexpected token < in JSON at position 0, and most answers written before then quote it. Current Chrome and Node.js print Unexpected token '<', "<!DOCTYPE "... is not valid JSON instead: the offending character in quotes, then up to the first ten characters of the text, cut off with three dots. That excerpt is the useful part. Ten characters are usually enough to recognise a doctype, a stray status line or the start of a sentence.

Firefox reports the same failure as an unexpected character at line 1 column 1, and Safari calls the angle bracket an unrecognized token. The Firefox wording has a page of its own, since it never shows the character at all.

Why a server answers JSON with HTML

  • The address is wrong. A typo in the path, a missing /api prefix or a relative URL resolved against the wrong page returns the site's 404 page. Development servers for single-page apps are the classic case: they answer every unknown path with index.html, so a misspelled endpoint comes back as the app's own HTML, with a status of 200.
  • The session expired. The request was redirected to a login form, and fetch followed the redirect without telling you.
  • Something in between failed. A proxy, a load balancer or a CDN answered with its own 502 or 504 page because the service behind it was down or slow.
  • The server crashed. A framework's debug page or a generic 500 page is HTML, even on a route that normally returns JSON.

Find out which one it is

Stop calling .json() blindly and look at what came back. Check the status and the Content-Type header first, and when either is wrong, read the body as text and put its beginning in the error:

const res = await fetch(url);
const type = res.headers.get('content-type') ?? '';
if (!res.ok || !type.includes('json')) {
  const body = await res.text();
  throw new Error(`${res.status} ${type}: ${body.slice(0, 200)}`);
}
const data = await res.json();

The browser's developer tools answer the same question without code. Open the Network panel, repeat the request and select it: the status column and the Response tab show the page the server actually sent, and the page's title often names the problem outright, such as 502 Bad Gateway or Sign in.

The same message with other characters

The token in the message tells you what the text started with, and each one points at a different mistake.

  • ' means single quotes, usually a Python dictionary or a JavaScript object literal; see Expected property name or '}'.
  • An invisible character at position 0 is a byte-order mark left at the start of a file.
  • "undefined" is not valid JSON means JSON.parse was handed undefined, which it turns into the text undefined before reading it. The variable you meant to parse was never filled in.
  • "[object Object]" is not valid JSON means the value was already an object. Something parsed it earlier, or it was never JSON text, so drop the second JSON.parse.

What this site does with it

Paste the text into the JSON Validator and the status bar reads Invalid JSON, with the first character underlined in Code view. The parser's own words are in the table's last row: it names the character it could not start a value with, at line 1, column 1.

Repair declines this one, and the refusal is deliberate. An HTML page often contains something that looks like JSON, a configuration blob inside a script tag for instance, and pulling that out would hand you a valid document that has nothing to do with the data you asked for. A wrong answer that parses is worse than an error, so the page is left alone and the question goes back to the request. For text that really is nearly JSON, the guide to fixing invalid JSON walks through what Repair can safely change.