Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

When response.json() or JSON.parse() throws an error such as Unexpected token '<', "<!DOCTYPE "... is not valid JSON, the quoted text is usually a short excerpt from the string the parser received. It is not proof of what caused the response: inspect the status, content type, final URL, redirects, and body to find out whether the server returned malformed JSON or something else, such as an HTML error page.

What the quoted text in a JSON error means

JSON parsing happens after a response body has been read or otherwise supplied as a string. V8’s parser may include a small context excerpt around the unexpected input in its error message. That excerpt can expose what text was passed to the parser, but it is not necessarily the entire response and does not identify why the server returned that content.

For example, a body that starts with <!DOCTYPE is likely HTML, not JSON. It might be an error page, login page, or application fallback. The opening characters alone do not tell you which one; check the response itself.

Why V8’s excerpt is described as 20 characters

V8’s current src/json/json-parser.cc implementation includes GetErrorMessageWithEllipses and a kMaxContextCharacters setting of 10. It selects a start, surrounding, or end context based on where parsing fails, and can add ellipses when it omits part of the input. The implementation does not mean every error always shows the first ten characters.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Coding Now reported an incremental measurement on Node.js v24.13.0 with V8 13.6: invalid inputs of 20 characters or fewer appeared whole, while inputs of 21 or more were abbreviated. For a failure at the first character, the longer-input excerpt was the first 10 characters followed by .... This is an observation for that runtime version, not a cross-version guarantee; other V8 releases and JavaScript engines may format errors differently. The V8 implementation is on the current main-branch source, which can change.

How to find out what the server returned

  1. Check the status and content type. In your browser’s Network panel, select the request and inspect its status code and Content-Type. A successful fetch promise does not mean the HTTP status was successful: fetch generally resolves for HTTP error responses, and parsing the body is a separate step.
  2. Check the final URL and redirects. Inspect the request’s final URL and whether a redirect was followed. For example, an authentication redirect can lead to a login page rather than the API response you expected.
  3. Read the body. Inspect the response content to distinguish an HTML error page, a fallback page, plain-text gateway response, empty body, or malformed JSON. A request’s Accept: application/json header does not guarantee that every response will contain JSON.

Common possibilities include a request resolving to the wrong relative path, a missing route, an authentication redirect, an SPA fallback, or a server error. Treat these as troubleshooting leads, not a definitive diagnosis: the metadata and body provide the evidence.

Log the response before parsing it

A response body is a stream. Reading it consumes it, so do not call res.json(), catch a parse failure, and then expect res.text() on the same response to return the body. If you need diagnostic text, read it once and parse that string.

async function fetchJson(url) {
  const res = await fetch(url);
  const contentType = res.headers.get("content-type") || "";
  const body = await res.text();

  // This substring check is illustrative; use the API's actual content-type contract.
  const looksLikeJson = contentType.toLowerCase().includes("application/json");

  if (!res.ok || !looksLikeJson) {
    console.error("Unexpected response", {
      status: res.status,
      contentType,
      url: res.url,
      redirected: res.redirected,
      bodyExcerpt: body.slice(0, 300)
    });
  }

  if (!res.ok) {
    throw new Error(`HTTP ${res.status} from ${res.url}`);
  }
  if (!looksLikeJson) {
    throw new Error(`Expected JSON; received ${contentType || "no Content-Type"}`);
  }

  return JSON.parse(body);
}

Adapt the content-type check and error handling to the endpoint’s contract. APIs can use JSON media types other than the exact string application/json, and a non-2xx response may intentionally carry a JSON error object worth parsing. Keep logged excerpts limited and avoid recording sensitive response data.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

How to interpret the evidence

Evidence What it suggests What to check next
Error excerpt begins with < or <!DOCTYPE The parser received text that looks like HTML, rather than a JSON document. Read the body; check status, content type, final URL, and redirect state to identify the page.
Non-success HTTP status The server returned an HTTP error response; fetch may still have resolved normally. Inspect its body and the API’s error-response contract before deciding whether to parse it as JSON.
Unexpected final URL or redirect The request may have reached a different route or destination than intended. Verify the relative URL, route, authentication state, and redirect target.
JSON content type but parsing still fails The body may be malformed, empty, or not actually JSON despite the header. Inspect the exact body and compare it with the endpoint’s expected response format.

A Next.js 16 development-server example reported by Coding Now illustrates why the body matters: a request to a missing route returned 404 Not Found, Content-Type: text/html; charset=utf-8, and an HTML page despite an Accept: application/json request header. That is an example from that server setup, not a promise about all Next.js responses.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.