Fix "SyntaxError: Unexpected token '<'": Robust API JSON Parsing & Error Handling in JavaScript
Summary: When your JavaScript code throws SyntaxError: Unexpected token '<', the server is often returning HTML (like a 404/500 page) instead of JSON. This article explains detection techniques, robust parsing patterns for fetch/axios/XMLHttpRequest, retry strategies, Content-Type checks, user-friendly messages, and monitoring tips — ready to copy/paste into production.
Why "Unexpected token '<'" happens and what it means
The error Unexpected token '<' occurs during JSON.parse or when the runtime tries to parse a response as JSON but encounters a character that's invalid in JSON — commonly the opening '<' of an HTML document. Typical causes are backend errors returning error pages, reverse-proxy 404/502 HTML, or an auth redirect to an HTML login page.
This is not a JavaScript parsing bug; it's a mismatch between expected payload type and received payload. The fix is both defensive client code and proper server-side content negotiation: the client must detect non-JSON responses, and the server must return appropriate JSON error bodies with correct headers (or consistent status codes).
Common misconfigurations: APIs that return HTML error pages with HTTP 200, services behind CDNs rewriting responses, or endpoints that return text/html by default. For diagnostics, always check the raw response body and HTTP headers (especially Content-Type).
Detecting non-JSON API responses reliably
Before attempting to parse, check the Content-Type header on the response. A JSON response should advertise something like application/json or application/problem+json. In fetch, this is available via response.headers.get('content-type'). If the header is missing or not JSON, treat the body as text and inspect it.
Even when the header says JSON, the body may still be HTML (some proxies set headers wrongly). A practical defensive approach: attempt to parse JSON inside a try/catch and, on error, fallback to reading the text to capture the HTML error payload for logging or user diagnostics.
Also validate status codes. If response.ok is false (HTTP 4xx/5xx), parse the error body as text first and attempt JSON only if content-type indicates JSON. This preserves meaningful server error messages and prevents unexpected parsing exceptions.
Robust parsing patterns (fetch, axios, and fallback)
Use a small utility that detects content type, safely attempts JSON.parse, and returns a predictable object: { ok, status, data, error, raw }. Keep it generic so you can reuse it across app and SSR boundaries.
// Fetch: safeJson helper
async function safeJsonFetch(url, opts = {}) {
const resp = await fetch(url, opts);
const ct = resp.headers.get('content-type') || '';
const raw = await resp.text(); // always capture raw body
if (!resp.ok) {
// Try JSON if header suggests it
try { return { ok: false, status: resp.status, error: ct.includes('json') ? JSON.parse(raw) : raw, raw }; }
catch (e) { return { ok:false, status:resp.status, error: raw, raw }; }
}
// If content-type claims json, parse; otherwise attempt parse and catch
if (ct.includes('json')) {
try { return { ok:true, status:resp.status, data: JSON.parse(raw), raw }; }
catch (e) { return { ok:false, status:resp.status, error: 'Invalid JSON', raw }; }
}
// Last resort: try parse, otherwise return text
try { return { ok:true, status:resp.status, data: JSON.parse(raw), raw }; }
catch (e) { return { ok:true, status:resp.status, data: raw, raw }; }
}
For axios, use responseType: 'json' and wrap parsing similarly. Axios will attempt JSON parse automatically, but when the server returns HTML with status 200, axios may still provide the HTML string as data—detect it and handle it as an error condition.
// Axios example
import axios from 'axios';
async function safeAxios(url, opts = {}) {
try {
const r = await axios({ url, ...opts, validateStatus: null });
const ct = (r.headers['content-type'] || '');
const raw = typeof r.data === 'string' ? r.data : JSON.stringify(r.data);
if (r.status >= 400) {
const parsed = ct.includes('json') ? tryParseJson(raw) : raw;
return { ok:false, status:r.status, error:parsed, raw };
}
if (ct.includes('json')) return { ok:true, status:r.status, data: tryParseJson(raw), raw };
const attempt = tryParseJson(raw);
return { ok:true, status:r.status, data: attempt ?? raw, raw };
} catch (err) {
return { ok:false, status: err.response?.status ?? 0, error: err.message, raw: err.response?.data };
}
}
function tryParseJson(s) {
try { return JSON.parse(s); } catch (e) { return null; }
}
Always capture the raw text for error reporting and logging. That raw HTML snippet usually contains the exact HTML error page that a server or CDN returned, which is invaluable for debugging why a JSON endpoint is returning HTML.
Retry mechanism for flaky API calls (with exponential backoff)
Transient errors (network hiccups, 502/503) deserve retries, but JSON parse failures caused by HTML responses should not be retried blindly: they often indicate the wrong endpoint or authentication error. Design your retry logic to only retry on network failures or 5xx server errors, not on 4xx or invalid-JSON results.
Implement exponential backoff with jitter. Use an upper cap on retries and log each attempt with the raw response on failure so you can correlate server-side logs. In production, integrate retries with circuit breakers to avoid overwhelming a struggling downstream service.
// Simple retry with exponential backoff and jitter
async function retry(fn, { retries = 3, baseMs = 200 } = {}) {
for (let i = 0; i <= retries; i++) {
try {
return await fn();
} catch (err) {
const shouldRetry = err?.status >= 500 || err?.code === 'ECONNRESET' || err?.name === 'FetchError';
if (!shouldRetry || i === retries) throw err;
const delay = Math.round(baseMs * Math.pow(2, i) * (0.5 + Math.random() / 2));
await new Promise(r => setTimeout(r, delay));
}
}
}
In practice, combine retry(fn) with safeJsonFetch so that transient network failures are retried but semantic errors (wrong content-type, HTML login/404) stop quickly and return useful diagnostics to the developer or operator.
User-friendly API error messages, logging, and monitoring
For users, show concise messages like "Server error, please try again later" and include an error code or reference ID. Avoid exposing raw HTML or stack traces to end users; instead log raw HTML and content-type to your error tracking system (Sentry, Datadog) for developer investigation.
When reporting errors from the client, attach the request URL, status code, content-type, and a snippet of the raw body (first 2KB). This helps operators reproduce and fix backend misconfigurations, such as endpoints returning HTML on 200 responses or missing JSON error payloads.
Automate alerts for unexpected content-type distributions: if more than X% of responses are text/html for an API that should serve JSON, trigger an incident. Monitor both parsing errors (client-side) and server-side error logs to close the feedback loop quickly.
Fixes on the server side: Content-Type, status codes, and consistent API contracts
The cleanest fix is server-side: ensure API endpoints always return a JSON body and set the header Content-Type: application/json; charset=utf-8 for success and error responses. For error responses, return a JSON error object and the appropriate HTTP status code (401, 404, 500), not an HTML page with 200.
If your API sits behind a proxy or CDN, check for custom error pages that might return HTML. Configure the proxy to pass-through or return JSON for backend errors. For authentication issues, avoid HTML login redirects on API routes; instead return 401/403 with JSON describing the auth problem.
Run automated contract tests (integration tests) that request each API endpoint and assert the content-type and JSON parseability. This prevents regressions where a framework-level exception handler or middleware accidentally returns HTML to an API consumer.
Quick checklist (what to check right now)
- Inspect raw response body and headers (Content-Type)
- Check HTTP status codes — do not parse on 4xx/5xx without safeguards
- Log raw body & headers to your monitoring (Sentry, Datadog)
- Implement safeJsonFetch/safeAxios and centralized retry policy
- Fix server to return JSON + correct Content-Type on API routes
People also ask / common questions
Below are typical user questions collected from search and developer forums; the three most relevant are answered in the FAQ section that follows.
- Why do I get "Unexpected token '<'" when parsing JSON in JS?
- How can I detect if an API returned HTML instead of JSON?
- How to gracefully handle API errors and show user-friendly messages?
- Should I retry JSON parsing errors or treat them as fatal?
- How do I check Content-Type header in fetch/axios?
- How to debug when a 200 response returns HTML for an API call?
- Can CORS or an auth redirect cause HTML to be returned to my SPA?
Selected FAQ
Q: Why am I getting "SyntaxError: Unexpected token '<' in JSON at position 0"?
A: That error means the parser saw an HTML document (starting with '<') instead of JSON. Most often the server returned an HTML error page (404/500) or an auth redirect. Check the response's Content-Type header and the raw body. Use a safe parser that reads the body as text first and then attempts JSON.parse inside a try/catch so you can log the HTML and the status code.
Q: How do I detect and handle non-JSON API responses in fetch or axios?
A: Inspect response.headers.get('content-type') (fetch) or response.headers['content-type'] (axios) before parsing. If the header contains "json", parse; otherwise read the .text() and treat it as an error or fallback. Always capture the raw text for diagnostics and only attempt retries on network/5xx errors, not on invalid-JSON results.
Q: Should I retry when JSON parsing fails with HTML returned?
A: Generally no. Parsing failures caused by HTML usually mean a logical error (wrong endpoint, auth redirect, server misconfiguration). Retries won't help unless the HTML is served intermittently due to upstream instability. Restrict retries to network errors and intermittent 5xx responses, and log or alert on invalid-content responses so the backend can be fixed.
Semantic core (keyword clusters)
Primary keywords: - SyntaxError Unexpected token '<' error - handling non-JSON API responses - API error handling in JavaScript - parsing JSON vs HTML responses Secondary keywords: - troubleshooting API JSON parsing errors - checking Content-Type header API - retry mechanism for API errors - user-friendly API error messages Clarifying / LSI phrases: - safe JSON parsing fetch axios - response.headers.get content-type - raw response body logging - exponential backoff retry with jitter - HTML returned instead of JSON - 404/500 HTML error pages API - Content-Type: application/json
Suggested micro-markup for SEO (FAQ schema)
Include the JSON-LD FAQ snippet below on the page to improve chances of featured snippets. It is already embedded at the end of this document.
