jsondecode.com logo

Fix: Invalid JSON Response from API

When an API call returns an HTML error page instead of JSON, calling res.json() or JSON.parse() throws a SyntaxError. Here is how to detect, handle, and debug it.

The Error Message

The most common form of this error looks like:

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

The < is the first character of an HTML page. The server returned something like a 404 page, a login redirect, a WAF block page, or a 500 error template — none of which are JSON.

Common Causes

HTTP StatusCauseResponse Body
404 Not FoundWrong URL or endpoint removedHTML 404 page
500 Internal Server ErrorUnhandled server exceptionHTML error template
401 UnauthorizedMissing or expired auth tokenHTML login page or error
302 RedirectSession expired, CORS issueHTML redirect or login page
200 OKServer misconfigured to return HTMLHTML with wrong Content-Type
503 Service UnavailableServer down or maintenanceHTML maintenance page

Safe fetch() Pattern

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

async function fetchJson<T>(url: string): Promise<T> {
  const res = await fetch(url);

  // Non-2xx status: read body as text to expose the error
  if (!res.ok) {
    const body = await res.text();
    throw new Error(
      `HTTP ${res.status} ${res.statusText}: ${body.slice(0, 200)}`
    );
  }

  // Guard against wrong Content-Type (e.g. accidental HTML response)
  const contentType = res.headers.get("content-type") ?? "";
  if (!contentType.includes("application/json")) {
    const body = await res.text();
    throw new Error(
      `Expected JSON but got ${contentType}: ${body.slice(0, 200)}`
    );
  }

  return res.json() as Promise<T>;
}

Axios Pattern

With Axios, non-2xx responses automatically throw, so you just need to handle the error:

import axios, { AxiosError } from "axios";

try {
  const { data } = await axios.get("/api/users");
  console.log(data);
} catch (err) {
  if (err instanceof AxiosError && err.response) {
    // err.response.data is the parsed body (may be HTML string)
    console.error("Status:", err.response.status);
    console.error("Body:", String(err.response.data).slice(0, 200));
  } else {
    throw err;
  }
}

Debug the Raw Response

If you are not sure what the server is returning, fetch the body as text and paste it below to inspect it:

const res = await fetch("/api/data");
const raw = await res.text();
console.log(raw); // paste this into the formatter below

Or open the browser DevTools → Network tab → click the request → Response tab to see the raw body.

Input JSON
Formatted Output
Formatted JSON will appear here

Frequently Asked Questions

Why does JSON.parse() throw 'Unexpected token <'?

The < character is the start of an HTML tag. This error means the server returned an HTML page (often a 404 Not Found or 500 Internal Server Error page) instead of JSON. JSON.parse() immediately fails because < is not a valid JSON character at position 0.

How do I check if a fetch() response is JSON before parsing?

Check two things: (1) res.ok — true only for 2xx status codes. (2) res.headers.get('content-type') — should contain 'application/json'. If either check fails, read the body as text with res.text() to see the actual error message from the server.

Does fetch() throw an error on 404 or 500 responses?

No. fetch() only rejects (throws) on network failures like DNS errors or CORS blocks. HTTP error status codes like 404, 500, or 401 still resolve successfully — res.ok will be false and res.status will be the error code, but no exception is thrown. You must check res.ok manually.

How does Axios handle non-JSON responses differently?

Axios throws an AxiosError for any non-2xx response, so 404 and 500 automatically land in the catch block. The error object has error.response.data (parsed body), error.response.status, and error.response.headers. This makes it easier to inspect the HTML error page without additional checks.

How can I see the raw response to debug an invalid JSON error?

Use the browser DevTools Network tab to inspect the raw response body — look at the Response tab for the request. Alternatively, call res.text() instead of res.json() and console.log the raw string. Then paste it into the JSON formatter on this page to identify what the server actually returned.

If jsondecode.com saved you time, share it with your team

Free forever. No ads. No sign-up. Help other developers find it.