Unexpected token '<', "<!DOCTYPE "... is not valid JSON
Your code expected JSON, but the server sent back an HTML page, usually a 404 page or your app’s own index.html.
Also appears as
- SyntaxError: Unexpected token < in JSON at position 0
- SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data
- SyntaxError: JSON Parse error: Unrecognized token '<'
Prompt to fix it
My app fails with "Unexpected token '<', "<!DOCTYPE "... is not valid JSON". A request expected JSON and got an HTML page. Find the fetch call that fails, work out which URL it really hits in development and in production, and what HTML comes back: the index.html fallback, a 404 page, a login redirect, or an error page. Fix the URL or the server route so it returns JSON, and set any missing API URL environment variable. Add a check that reads the body as text and reports the status when a response isn’t JSON. Don’t just wrap JSON.parse in a try/catch that hides it.
Paste into Layerwise or any AI coding tool.
What it means
HTML pages begin with <!DOCTYPE html>, so the < is the first character of a web page. Your code called response.json() or JSON.parse(), the response was HTML instead of data, and the parser failed on the very first character.
The HTML is usually a page you’d recognize. Single-page apps are often set up to answer unknown URLs with index.html so client-side routing works, and the Vite dev server does this by default. When no API route exists at the URL you called, that fallback quietly answers instead, often with status 200. Other times it’s a 404 page, a sign-in page, or the host’s error page.
To see which one, open the failing request in the Network tab and look at its Response. The page title usually tells you. The fix is to call the right URL or make the server return JSON, and reading the body as text before parsing makes the next mismatch obvious.
Common causes
- The API path is wrong, or the route was never deployed, so the host’s single-page fallback returned
index.html. - The frontend calls a relative URL like
/api/items, which the Vite dev proxy forwards locally but the static host answers in production. - The server returned an HTML error page, such as a 404, 500, or gateway error, and the code parsed it without checking
response.ok. - A sign-in check redirected the request to a login page, and
fetchfollowed the redirect. - The API URL environment variable is missing in production, so the code requests a path like
/undefined/items.
How to fix it
- In the Network tab, select the failing request and open Response to see which HTML page came back and from which URL.
- Compare that URL with where your API actually lives. Fix the path, or set the API base URL environment variable on your host and redeploy.
- If the dev setup proxies
/apithrough Vite, route/apito the same backend in production, or call the backend’s full URL. - Check
response.okand theContent-Typeheader before callingresponse.json(), and log the text body when either looks wrong. - Make the server return JSON for its errors too, with
Content-Type: application/json.
Example fix
export async function getJson<T>(url: string): Promise<T> {
const response = await fetch(url);
const type = response.headers.get('Content-Type') ?? '';
if (!response.ok || !type.includes('application/json')) {
const text = await response.text();
throw new Error(
`Expected JSON from ${url}, got ${response.status} ${type}: ${text.slice(0, 200)}`
);
}
return response.json() as Promise<T>;
}Related errors
All errorsUnexpected end of JSON input
Your code tried to read JSON from text that was empty or cut off, so the parser ran out of input before the data was complete.
404 on page refresh
Your single-page app works from the home page, but the host has no file for deeper URLs, so reloads and shared links hit a 404.
Failed to load module script
The browser asked for your app’s JavaScript and got an HTML page back, so it refused to run it and the page stayed blank.
Blocked by CORS policy
The browser hid a response from another site because that server never said your site is allowed to read it.
Build with an agent that checks its work
The Layerwise agent clicks through the live preview and reads the console, so it catches errors before you do.