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

```text
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:

```js
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](/http/404) or [500](/http/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](/http/204) 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](/tools/json/json-validator) to find the exact position.

## Step 3: guard your fetch calls

Check the status and the [Content-Type](/headers/content-type) before parsing, and include the start of the body in the error so the next failure explains itself:

```js
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`](/mime/json).

## The localStorage variant

```js
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:

```js
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](/tools/json/json-formatter).
