Fix ‘Unexpected Token’ in JSON: A Complete Developer’s Diagnostic Guide
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.
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:
| Error Message / Symptom | Likely Cause | First Thing to Check |
|---|---|---|
Unexpected token < |
HTML returned instead of JSON (usually a server crash or login redirect) | Network tab response body |
Unexpected token u |
Parsing the literal string "undefined" |
Inspect your variable before passing it to JSON.parse() |
Invalid value / Unexpected token |
Invalid data type passed to JSON.parse() |
Check the variable’s type before parsing |
Unexpected token N |
Parsing NaN or a non-JSON literal value |
Inspect raw payload data |
Unexpected end of JSON input |
Truncated or completely empty server response | Check response length and network completion status |
Unexpected string or number |
Broken quotes, missing commas, or invalid numeric syntax | Validate JSON syntax structure |
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})`);
}
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 a200 OKstatus 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.
- 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);
}
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;
}
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;
}
}
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. - Truncated Responses: Server timeouts or network interruptions can cut a JSON payload short, resulting in an
Unexpected end of JSON inputerror. - 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:
- HTTP Status: Did the server return
200 OK, or an error code like500or404? - Final URL: Did
response.redirectedflag a silent login redirect? - Content-Type Header: Does the header explicitly match
application/json? - Raw Response Body: Log the output via
response.text()instead ofresponse.json(). - First Character: Inspect character index
[0]to see if it starts with an unexpected tag like<. - JSON Validator: Paste the exact string payload into our free online JSON validator to expose structural anomalies.
- Server-Side Logs: Trace the backend stack logs to see what exception occurred during generation.

One Comment