devkult_
tools29converters34
home/blog/unexpected-token-in-json
javascriptjsondebugging

Unexpected Token in JSON at Position 0: What It Means and How to Fix It

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

Older browsers and Node versions word it as Unexpected token < in JSON at position 0. Either way, the message is telling you something specific: the first character of what you tried to parse as JSON was < — and JSON never starts with <. You received HTML, not JSON. The parser is fine; the response isn't what you think it is.

The messages, decoded

Message What you actually received
Unexpected token '<', "<!DOCTYPE "... An HTML page — usually an error page, a login page or your app's index.html
Unexpected end of JSON input An empty body
"[object Object]" is not valid JSON An object you already parsed, passed to JSON.parse again
"undefined" is not valid JSON The string "undefined" — often from localStorage
Unexpected token '' (an invisible character) A UTF-8 byte order mark before the JSON
Expected double-quoted property name A trailing comma, or single-quoted keys

Firefox says JSON.parse: unexpected character at line 1 column 1 of the JSON data, and Safari says JSON Parse error: Unrecognized token '<'. Python's equivalent is JSONDecodeError: Expecting value: line 1 column 1 (char 0), which requests raises from .json().

Step 1: look at what you got

Read the body as text before parsing it:

const res = await fetch("/api/items");
const text = await res.text();
console.log(res.status, res.headers.get("content-type"), text.slice(0, 200));

Or open DevTools → Network, click the request and look at the Response tab. Nine times out of ten, the answer is right there.

Step 2: match it to a cause

An HTML error page. The request hit a 404 or 500, and the server returned its HTML error page. Check the status code — response.json() doesn't care whether the request succeeded.

Your app's own index.html. Single-page apps return index.html for every unknown path, so a wrong API URL (/api/item instead of /api/items, or a missing /api prefix in production) gets HTML with status 200. Also check your dev server proxy: in development /api may be forwarded to the backend, but in production it isn't.

A login page. An expired session redirects to /login; fetch follows the redirect and hands you the login HTML. res.redirected will be true.

An empty body. 204 No Content responses and some error responses have no body, so there's nothing to parse — "Unexpected end of JSON input". Check the status before calling .json().

Parsing twice. response.json() and axios already return objects. Calling JSON.parse() on the result turns it into the string "[object Object]" first, which fails.

Not quite JSON. Hand-written JSON with trailing commas, comments, single quotes or NaN isn't valid. Paste it into the JSON validator to find the exact position.

Step 3: guard your fetch calls

Check the status and the Content-Type before parsing, and include the start of the body in the error so the next failure explains itself:

async function getJson(url, options) {
  const res = await fetch(url, options);
  const type = res.headers.get("content-type") ?? "";
  if (!res.ok || !type.includes("application/json")) {
    const body = await res.text();
    throw new Error(`${res.status} from ${url} (${type || "no content type"}): ${body.slice(0, 120)}`);
  }
  return res.json();
}

On the server side, make your API return JSON errors with Content-Type: application/json — including for 404s and 500s — so clients get a parseable message instead of an HTML page. The correct type for JSON is application/json.

The localStorage variant

JSON.parse(localStorage.getItem("settings")); // fine when the key exists

When the key doesn't exist, getItem returns null, and JSON.parse(null) happens to return null — no error. But if some code once saved localStorage.setItem("settings", undefined), the stored value is the string "undefined", which throws. Guard and reset:

function readSettings() {
  try {
    return JSON.parse(localStorage.getItem("settings")) ?? {};
  } catch {
    localStorage.removeItem("settings");
    return {};
  }
}

To inspect or tidy a JSON payload by hand, use the JSON formatter.

More from the blog