When an application throws a JSON syntax error, it usually halts execution immediately. Whether it’s SyntaxError: Unexpected token < in JSON at position 0 or a sudden crash on a production endpoint, tracking it down requires moving methodically from the network layer down to the raw string.

This guide covers why these errors happen, how to diagnose them using proper debugging techniques, and how to fix both server responses and malformed syntax — with every error below reproduced in a real run, not just quoted from memory.

Tested: The Error, Reproduced Live

Before the theory, here’s the error happening for real. We started a small local server, requested a JSON endpoint that doesn’t exist, and got an HTML 404 page back instead — exactly what happens in production when an API route is wrong or a server crashes. Then we did what frontend code usually does: called response.json() on it.

The server’s response, via curl -i:

HTTP/1.0 404 File not found
Server: SimpleHTTP/0.6 Python/3.12.3
Content-Type: text/html;charset=utf-8
Content-Length: 335

<!DOCTYPE HTML>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Error response</title>

Note the two facts that matter: the status is 404, and the Content-Type is text/html, not application/json. The body starts with <!DOCTYPE HTML> — the very first character is <, which is not valid JSON.

What Node.js actually printed when we fetched it and parsed the body:

status: 404 File not found
content-type: text/html;charset=utf-8
raw body preview (first 120 chars): "<!DOCTYPE HTML>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>Error response</title>\n    </h"
JSON.parse error: Unexpected token '<', "<!DOCTYPE "... is not valid JSON

And calling response.json() directly — the line most frontend code actually runs — throws the identical error:

res.json() error: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
error name: SyntaxError

One wording note: the exact message depends on your runtime. Modern Node.js (v24, used for these runs) says Unexpected token '<', "<!DOCTYPE "... is not valid JSON. Older browsers and older Node versions phrase the same failure as Unexpected token < in JSON at position 0 — same error, same cause, different wording. “Position 0” just means the very first character of the response is already wrong, which is the giveaway that the body isn’t JSON at all.

Quick Diagnostic Reference Table

If you already know the specific error message your console is throwing, use this reference table to jump straight to the cause. The “what we actually saw” column is from real runs in Node.js v24:

Error Message / SymptomLikely CauseFirst Thing to CheckWhat We Actually Saw (Node.js v24)
Unexpected token <HTML returned instead of JSON (usually a server crash or login redirect)Network tab response bodyUnexpected token '<', "<!DOCTYPE "... is not valid JSON
Unexpected token uParsing the literal string "undefined"Inspect your variable before passing it to JSON.parse()"undefined" is not valid JSON
Unexpected token NParsing NaN or a non-JSON literal valueInspect raw payload data"NaN" is not valid JSON
Unexpected end of JSON inputTruncated or completely empty server responseCheck response length and network completion statusUnexpected end of JSON input (for an empty string)
Unexpected string or numberBroken quotes, missing commas, or invalid numeric syntaxValidate JSON syntax structureSingle quotes: Expected property name or '}' in JSON at position 1

Step 1: Check HTTP Status Codes Before Parsing

A common mistake in modern frontend code is chaining response.json() directly onto a fetch request without verifying what the server actually returned:

// Dangerous: assumes the server always returns valid JSON
const data = await response.json();

If the server encounters a fatal error, it might return a 500 status code alongside an HTML stack trace. If you call response.json() on that HTML payload, the browser throws an unexpected token error. Always validate the response status and headers first:

const response = await fetch("/api/data");

// Always check if the HTTP status is successful first
if (!response.ok) {
    throw new Error(`HTTP error! Status: ${response.status} (${response.statusText})`);
}

In our reproduced run, this one check would have caught the problem immediately: response.ok was false (status 404), and the Content-Type was text/html. The parse error was never a JSON problem — it was a wrong-endpoint problem, and the status code said so before a single character was parsed.

Keep these HTTP status codes in mind during your investigation:

  • 200 OK: The request succeeded, but the body content might still be malformed text.
  • 301 / 302 Redirects: The request triggered a redirect. Note that fetch() follows redirects automatically, meaning your JavaScript might ultimately receive a 200 OK status with an HTML login page body rather than a visible 302 response.
  • 401 / 403 Forbidden: Authentication or authorization failures. While many APIs return a clean JSON error object (e.g., {"error": "Unauthorized"}), you should never assume the format — always inspect the actual response body.
  • 404 Not Found: The endpoint path is wrong, often returning a default server 404 HTML page. This is exactly what our reproduced run returned.
  • 500 Internal Server Error: A backend crash that dumps raw server stack traces into the response body.

Real-World Case Study: Tracking Silent Redirects

One of the most elusive causes of parsing errors happens when an authenticated API route redirects an expired session.

Because the browser’s fetch() API follows redirects automatically, a 302 status code is transparently handled behind the scenes. If your session expired, the server redirects the request to a login page. Your script receives a 200 OK response code, but the body is an HTML document starting with <!DOCTYPE html>.

When debugging unexpected redirects, always check where the fetch request actually ended up by inspecting the response properties:

const response = await fetch("/api/user-data");

// Check if fetch followed a redirect chain
if (response.redirected) {
    console.warn("Request was redirected to:", response.url);
}

This is the one case where the Step 1 status check won’t save you — the final status is 200. The giveaway is response.redirected being true and response.url pointing at a login page instead of your API endpoint. And the body still starts with <, so the same first-character check from the diagnostic table applies.

Step 2: Isolate the Raw Response Body

If you suspect an endpoint is returning the wrong data format, stop using .json(). Instead, read the payload as raw text and log it to your console to inspect the leading characters:

const response = await fetch("/api/data");

console.log("Status:", response.status);
console.log("Final URL:", response.url);
console.log("Content-Type:", response.headers.get("content-type"));

// Grab the body as text to inspect it safely
const rawText = await response.text();

console.log("Raw response preview:", rawText.slice(0, 100));

try {
    const data = JSON.parse(rawText);
    return data;
} catch (error) {
    console.error("Failed to parse JSON. Inspecting raw response data.");
    throw error;
}

This is the exact procedure we used in the reproduced run above. The three lines that diagnose the problem are the status, the content-type, and the first 100 characters of the body. In our run those three lines were 404, text/html, and <!DOCTYPE HTML> — diagnosis complete before JSON.parse is ever called.

Step 3: Programmatically Inspect Offending Characters with Safe Guards

When dealing with a malformed string or a file load error where the position index is vague, you can use a guarded utility to log the exact character layout without crashing on invalid inputs:

function debugJSONParse(rawData) {
    console.log("Received type:", typeof rawData);

    if (typeof rawData !== "string") {
        console.error("Expected a string but received:", rawData);
        throw new TypeError("JSON.parse() requires a string-like JSON input.");
    }

    try {
        return JSON.parse(rawData);
    } catch (error) {
        console.error("--- JSON Parse Diagnostic ---");
        console.error("Error message:", error.message);
        console.error("Raw response length:", rawData.length);
        console.error("First character:", rawData[0], "charCode:", rawData.charCodeAt(0));
        console.error("First 30 characters:", rawData.slice(0, 30));
        console.error("-----------------------------");
        throw error;
    }
}

Tested: the other error variants, one by one

We ran the remaining cases from the diagnostic table through the same parser, so you can match your console output to a cause:

undefined (string) => SyntaxError: "undefined" is not valid JSON
NaN => SyntaxError: "NaN" is not valid JSON
empty string => SyntaxError: Unexpected end of JSON input
truncated {"a":1 => SyntaxError: Expected ',' or '}' after property value in JSON at position 6 (line 1 column 7)
BOM + {} => SyntaxError: Unexpected token '', "{}" is not valid JSON
single-quoted => SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
trailing comma => SyntaxError: Expected double-quoted property name in JSON at position 7 (line 1 column 8)

Two of these deserve a closer look. The truncated payload {"a":1 doesn’t say “unexpected end” — it points at the exact position where the parser ran out of input (position 6), which tells you the response was cut short rather than empty. And the BOM case is the nastiest: the offending character is invisible. The payload looks like {} to the human eye, but its first character has charCode 65279 (the UTF-8 byte order mark, \ufeff), and the parser rejects it at position 0. That’s the case where the charCodeAt(0) line in the utility above earns its keep — rawData[0] prints as nothing, but the char code gives the culprit away.

When the JSON “Looks” Valid

Sometimes a payload fails parsing even when it appears completely correct to the human eye. In these scenarios, look out for the following hidden culprits:

  • Hidden BOM Headers: A UTF-8 Byte Order Mark (\ufeff) prepended by text editors breaks strict parsers at position zero. We reproduced this above: the character is invisible, its charCode is 65279, and the parser fails on it before it ever sees the {.
  • Truncated Responses: Server timeouts or network interruptions can cut a JSON payload short, resulting in an Unexpected end of JSON input error — or, as our run showed, a position-specific error pointing at exactly where the input ran out.
  • Server Pollution: Debug logs, warning notices, or whitespace accidentally echoed out by a backend script before the JSON object begins.
  • Mismatched Content-Type: The API claims to return JSON in documentation, but actually returns plain text or HTML.
  • CDN / Proxy / WAF Interference: Cloudflare or enterprise firewalls intercepting bad requests and injecting HTML challenge pages or error templates.
  • Type Confusion: Passing a live JavaScript object straight into JSON.parse() instead of operating on raw text strings.

The Definitive Diagnostic Methodology

When an unexpected parsing error hits production, stop guessing and walk through this exact troubleshooting sequence:

  1. HTTP Status: Did the server return 200 OK, or an error code like 500 or 404?
  2. Final URL: Did response.redirected flag a silent login redirect?
  3. Content-Type Header: Does the header explicitly match application/json?
  4. Raw Response Body: Log the output via response.text() instead of response.json().
  5. First Character: Inspect character index [0] to see if it starts with an unexpected tag like <.
  6. JSON Validator: Paste the exact string payload into our free online JSON validator to expose structural anomalies.
  7. Server-Side Logs: Trace the backend stack logs to see what exception occurred during generation.

Frequently Asked Questions

What does “Unexpected token < in JSON at position 0” actually mean?

It means the very first character of the text you tried to parse is <, which can’t start a JSON value. In practice that almost always means the server returned an HTML page — a 404 page, a 500 error page, or a login page after a redirect — instead of JSON. The fix is at the network layer (wrong URL, expired session, server crash), not in your JSON.

Why does the error message look different in different browsers?

Each JavaScript engine phrases it differently. Modern Node.js says Unexpected token '<', "<!DOCTYPE "... is not valid JSON; older browsers say Unexpected token < in JSON at position 0. Same error, same cause. Match on the token character (<, u, N) rather than the exact wording.

How do I see what the server actually returned?

Stop calling response.json() and read the body as text instead: const rawText = await response.text(), then log the status, the Content-Type header, and the first 100 characters. In our reproduced run, those three lines — 404, text/html, <!DOCTYPE HTML> — diagnosed the problem completely.

Can this error happen if the server returns a 200 OK?

Yes. A 200 with an HTML body happens when a request is silently redirected to a login page (check response.redirected and response.url), or when a proxy or firewall injects an HTML challenge page. The status check alone won’t catch it — the content-type and the first character will.

My JSON looks perfect but still fails to parse. What now?

Check for invisible characters. A UTF-8 BOM (\ufeff, charCode 65279) at the start of the file fails parsing at position 0 while looking like nothing at all — we reproduced exactly that above. Log rawData.charCodeAt(0): if it’s 65279, strip the BOM before parsing. Truncation is the other invisible cause: compare the response’s Content-Length header against the actual body length.

Summary

Unexpected token < in JSON at position 0 is almost never a JSON problem — it’s a network problem wearing a JSON error message. We reproduced it live: a request to a missing endpoint returned a 404 HTML page, and response.json() threw on the very first character. Check the status code, the content-type, and the first 100 characters of the raw body before you touch your JSON — those three checks diagnose the cause in seconds. For the rarer cases where the JSON genuinely is malformed, the position in the error message points at the exact character, and an invisible BOM (charCode 65279) is the usual suspect when the payload looks perfect but still fails.

Similar Posts

One Comment

Leave a Reply

Your email address will not be published. Required fields are marked *