Skip to content

JavaScript · browsers and Node.js

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

Unexpected token '<', "<!DOCTYPE "... is not valid JSON means the server sent an HTML page where your code expected JSON.

Updated · 2 min read

console
Chrome, Edge, Node.jsSyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
Node.js 18, older ChromeSyntaxError: Unexpected token < in JSON at position 0
FirefoxSyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data
SafariSyntaxError: JSON Parse error: Unrecognized token '<'

Find it in your JSON

Paste the text that failed. The checker points at the exact spot, and Repair fixes most syntax mistakes.

Input · JSON

Invalid JSON

Line 1, column 1

Unexpected '<': this looks like HTML or XML, not JSON.

1<!DOCTYPE html> ^2<html lang="en">

Fix: A server often sends an HTML error or login page where JSON was expected. Check the URL, the status code and the Content-Type header.

Why this happens
Invalid JSONRuns in your browser

Load from a URL

Your browser fetches it directly, so the server must allow cross-origin requests (CORS).

Why it happens

JSON never starts with <; HTML almost always does. Most common sources first:

1. A wrong URL returned a 404 page. The checker holds Express’s default 404 response, Cannot GET /api/invoices. Look for a missing /api prefix, a typo, or a relative path: fetch('api/invoices') on /dashboard/ requests /dashboard/api/invoices.

2. The frontend server answered instead of the API. Single-page app dev servers and hosts return index.html for unknown paths, with status 200, so res.ok alone does not catch it. A lowercase "<!doctype "... is the same case.

3. The server or a proxy failed. An app crash, or a 502 Bad Gateway from nginx or a load balancer while the backend is down, returns an HTML error page.

4. A login redirect. An expired session or a missing token redirects to a sign-in page, and fetch() follows it. Captive portals on hotel and office networks do the same.

How to fix Unexpected token ‘<’

Log the status, the final URL and the first characters of the body; together they name the cause. To inspect a copied response, paste it into the JSON validator, which reports HTML as HTML.

fetch

Check the status and the Content-Type before calling res.json():

const res = await fetch('/api/invoices');
const type = res.headers.get('content-type') ?? '';

if (!res.ok || !type.includes('application/json')) {
  const preview = (await res.text()).slice(0, 150);
  throw new Error(`${res.status} from ${res.url} (${type}): ${preview}`);
}
const invoices = await res.json();

res.url is the address after redirects, so a login redirect shows up as /login, and res.redirected is true.

axios

axios rejects 4xx and 5xx responses itself, so HTML that reaches your code came with a 200: an SPA fallback or a login page. It does not throw on that HTML. The string lands in response.data, and the failure surfaces later as something like data.map is not a function. Check the type:

const { data, headers } = await axios.get('/api/invoices');
if (typeof data === 'string') {
  throw new Error(`Expected JSON, got ${headers['content-type']}: ${data.slice(0, 150)}`);
}

Dev server proxy

In Vite, forward API paths to the backend:

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
  server: {
    proxy: { '/api': 'http://localhost:3000' },
  },
});

Create React App uses a "proxy": "http://localhost:3000" field in package.json. In production, put the host’s catch-all rewrite to index.html after the API routes.

Express

Return JSON for unknown API routes. Register this after them:

app.use('/api', (req, res) => {
  res.status(404).json({ error: `No route for ${req.method} ${req.originalUrl}` });
});

Expired sessions

Treat 401, 403 and res.redirected as signed out: send the user to the login page instead of parsing.

Same error in other languages

Other messages and what they mean: common JSON errors.

Questions

What does the text in quotes tell me?

It is the first 10 characters of what you received. "<!DOCTYPE " is a full HTML page, "<html><bod" a bare one, and a letter, as in "Internal S"..., a plain-text error. Bodies of 20 characters or fewer are quoted whole.

Is this a CORS error?

No. A CORS failure rejects fetch() with TypeError: Failed to fetch before any body is readable. Here a body arrived; it is just not JSON.

Why does the request work in Postman but not in my app?

Postman calls the full URL you typed. In the browser, /api/invoices resolves against the page's origin, often your dev server or static host. Log res.url to see where the request went.

Related errors

All common JSON errors

Esc