“Unexpected token < in JSON at position 0”

Guide · Updated July 2026 · ~8 min read

This is the most-searched JSON error in the world, and almost everyone starts by debugging the wrong thing. The message is not telling you that your JSON is malformed. It is telling you that what came back was never JSON at all — it was HTML.

How to read the message

Every part of the error is a clue, and together they narrow the cause down to almost one thing:

Put together: your code asked for JSON, received an HTML document, and handed that document to JSON.parse. The JSON is innocent. The response is the problem.

See the actual response — this is the whole fix

Stop guessing and print what really arrived. Read the body as text instead of JSON, just once, and the cause is usually obvious immediately:

const res = await fetch('/api/users');

// Read as text FIRST — .json() would throw before you can look
const body = await res.text();
console.log(res.status, res.headers.get('content-type'));
console.log(body.slice(0, 300));

Nine times out of ten the console now shows something like this, and you have your answer:

404 text/html
<!DOCTYPE html>
<html><head><title>404 Not Found</title></head>
<body><h1>Not Found</h1>…

The five causes, in order of likelihood

1. The URL is wrong and you got a 404 page

By far the most common cause. A typo in the path, a missing /api prefix, or a relative URL resolving against the wrong base. The server answers with its HTML 404 page, which starts with <. Check res.status — if it is 404, this is your bug.

2. The server threw a 500 and returned an HTML error page

Frameworks in development mode return richly formatted HTML stack traces. Your endpoint exists but crashed. The fix is in the server logs, not the client.

3. Your session expired and you were redirected to a login page

An authenticated API returns a 302 to /login, fetchfollows the redirect transparently, and the login page's HTML lands in your parser. The telltale sign is that it works right after signing in and breaks later. Have the API return401 with a JSON body for unauthenticated requests instead of redirecting.

4. A dev server or SPA fallback served index.html

Single-page-app dev servers are configured to return index.html for any unmatched route so client-side routing works. That catch-all happily swallows your API call too if the proxy is not set up, so every unmatched request returns your app shell. Check your proxy configuration.

5. It genuinely is XML

Some APIs return XML unless you ask otherwise. Send an explicitAccept: application/json header — or, if XML is all the service offers, run it through our XML to JSON converter.

Write the guard that prevents it forever

The root problem is that fetch does not reject on HTTP error statuses, so failures flow straight into the parser. Check the status and content type before parsing, and you get an error that names the real problem:

async function getJson(url) {
  const res = await fetch(url, { headers: { Accept: 'application/json' } });

  if (!res.ok) {
    throw new Error(`Request failed: ${res.status} ${res.statusText}`);
  }

  const type = res.headers.get('content-type') ?? '';
  if (!type.includes('application/json')) {
    const preview = (await res.text()).slice(0, 120);
    throw new Error(`Expected JSON but got ${type}. Body starts: ${preview}`);
  }

  return res.json();
}

“Expected JSON but got text/html. Body starts: <!DOCTYPE html>” is a message you can act on at 3am. “Unexpected token < in JSON at position 0” is not.

The same error in every runtime

The wording changed in 2023, so search results may not match what you see. All of these mean exactly the same thing:

Other tokens, other causes

When the token is not <, the character itself tells you which mistake you made:

If the token points somewhere in the middle of a genuine JSON document, the problem really is syntax — our common JSON errors guide covers all ten, and the JSON validator underlines the exact line and column.

Checklist

  1. Log res.status and the first 300 characters of res.text().
  2. Does the body start with <!DOCTYPE or <html? Then it is HTML, not JSON.
  3. 404 → fix the URL. 500 → read the server logs. 302 to login → fix auth handling.
  4. Status 200 but still HTML? Your dev-server proxy is serving the SPA fallback.
  5. Add the res.ok and content-type guard so the next failure names itself.

Frequently asked questions

Why does the error say “position 0”?

Position 0 is the very first character of the response. If the parser fails there, nothing about the response was ever JSON — it is not a small syntax slip further down the document.

Why is the token a “<” specifically?

Because < is the first character of <!DOCTYPE html> or <html>. Your request returned an HTML page — usually an error page, a login redirect, or a dev-server fallback.

I get “Unexpected token o in JSON at position 1” instead. Same thing?

Same family, different cause. That one means you passed an object to JSON.parse. JavaScript stringifies it to [object Object] first, and position 1 is the letter “o”. The data was already parsed — delete the extra JSON.parse call.

Does this mean my API is down?

Not necessarily. A 404, a 500, an expired session redirect, and a proxy misconfiguration all produce HTML. Log the status code alongside the body text to tell them apart.

Keep reading