Blog
3 min read

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

Your code expected JSON and got HTML — almost always an error page or your app's index.html. Why it happens (wrong URL, 404, server error, SPA fallback, login redirect), how to see what the server actually sent, and how to parse responses safely.

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

(older browsers: Unexpected token < in JSON at position 0)

This error looks cryptic but it's telling you something precise: your code tried to read a response as JSON, and the very first character was <. JSON starts with { or [. The thing that starts with < is HTML — <!DOCTYPE html> or <html>.

So the server sent back a web page when your code expected data. The job is to find out which page and why.

See what the server actually sent

Open dev tools → Network tab, find the request, and click Response (or Preview). You'll see the HTML. It's usually one of:

  • Your app's home page (index.html)
  • A 404 "Not Found" page
  • A 500 error page
  • A login page
  • A hosting provider's error page

Also check the status code column. That alone often explains it. (HTTP status codes)

Cause 1: The URL is wrong (and the SPA fallback hides it)

This is the most common one. Your frontend calls /api/user but the API is at /api/users, or on a different server entirely. Single-page apps are configured to serve index.html for any unknown URL (so that client-side routes work on refresh — see why). So instead of a clean 404, the server returns your homepage HTML with a 200.

Fix: correct the URL. Check the "Request URL" in the Network tab — look for undefined in it, a missing /api prefix, or localhost in production.

Cause 2: The API isn't running in this environment

In development, Vite proxies /api to your backend. In production, there may be no backend at that path at all — your static host returns its fallback page instead. Deploying a frontend without its backend causes exactly this. (Why your app works locally but not in production)

Cause 3: The server errored

The backend crashed and returned an HTML error page (a 500 or a 502). Look at the server logs for the real error.

Cause 4: You're being redirected to a login page

The API requires authentication, the request didn't include a valid session, and the server redirected to /login — which fetch follows automatically, returning the login page's HTML. Check that cookies are sent (credentials: 'include' for cross-origin requests) or that the auth header is set.

Cause 5: The response isn't JSON at all

Sometimes it's not HTML but plain text, an empty body (Unexpected end of JSON input), or a file. Calling res.json() on an empty 204 No Content response throws.

Parse responses defensively

Don't call .json() blindly. Check status and content type first:

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

if (!res.ok) {
  const text = await res.text()
  throw new Error(`API ${res.status}: ${text.slice(0, 200)}`)
}

const type = res.headers.get('content-type') || ''
if (!type.includes('application/json')) {
  throw new Error(`Expected JSON, got ${type}`)
}

const users = await res.json()

Now when it fails, the error message tells you the status code and shows the start of what came back — which is usually enough to see the problem immediately.

On the server side

Make your API return JSON for errors too, with the right status code:

return Response.json({ error: 'Not found' }, { status: 404 })

and make unknown /api/* routes return a JSON 404 rather than falling through to the HTML fallback.

The summary

  • < at position 0 means you received HTML, not JSON.
  • Check the Network tab: the response body and status code tell you which page it was.
  • Usual causes: wrong URL hidden by SPA fallback, missing backend, server error, login redirect.
  • Check res.ok and the content type before calling .json().

EasySpawn runs your frontend and backend together on one server with the same routes in development and production, so /api means the same thing everywhere. See how it works or join the waitlist.

Related: What Is JSON? · Failed to Fetch · Why Refreshing Your React Page Gives a 404 · How to Read an Error Message

Keep reading