The error SyntaxError: Unexpected token ‘<‘ , “<!DOCTYPE”… is not valid JSON (or “Unexpected token in JSON at position 0”) means you tried to parse something that isn’t JSON โ usually an HTML error page or an empty response. Here’s how to diagnose and fix it.
๐ Table of Contents
What This Error Means
You called JSON.parse() or response.json() on a response that isn’t valid JSON. The “position 0” and “<” clues are key: position 0 means the very first character is wrong, and “<” means the response starts with HTML (like <!DOCTYPE html>) โ you got an HTML error page instead of JSON.
Cause 1: Server Returned an HTML Error Page
// ๐ The API returned a 404/500 HTML page, not JSON
const response = await fetch('/api/data');
const data = await response.json(); // โ fails - response is HTML
// The response body is something like:
// 404 Not Found ...
// JSON.parse chokes on the '<' at position 0
// โ
Check the response status and content type first
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const contentType = response.headers.get('content-type');
if (!contentType?.includes('application/json')) {
const text = await response.text();
throw new Error(`Expected JSON but got: ${text.slice(0, 100)}`);
}
const data = await response.json();
Cause 2: Empty Response Body
// ๐ Parsing an empty response (e.g., 204 No Content)
const data = await response.json(); // โ fails if body is empty
// โ
Handle empty bodies
const text = await response.text();
const data = text ? JSON.parse(text) : null;
// โ
Or check status
if (response.status === 204) {
return null; // No Content - nothing to parse
}
const data = await response.json();
Cause 3: Wrong URL / API Endpoint
// ๐ Wrong URL returns your app's HTML (SPA fallback) instead of API JSON
fetch('/api/users') // typo or wrong path
// If /api/users doesn't exist, the server may return index.html
// (common with SPAs) - which starts with '<' -> parse error
// โ
Verify the URL is correct and the endpoint exists
// Check the Network tab to see what the response actually is
// Ensure API routes are registered and the path is exact
Cause 4: Double-Parsing Already-Parsed JSON
// ๐ The data is already an object, but you parse it again
const data = await response.json(); // already parsed to an object
const parsed = JSON.parse(data); // โ parsing an object fails
// โ
response.json() already parses - don't parse again
const data = await response.json(); // done - it's an object
// axios also auto-parses:
const { data } = await axios.get('/api'); // data is already parsed
Cause 5: Trailing Characters or Invalid JSON
// ๐ The JSON has a syntax error (trailing comma, single quotes)
JSON.parse('{"name": "Alice",}'); // โ trailing comma
JSON.parse("{'name': 'Alice'}"); // โ single quotes not valid JSON
JSON.parse('{name: "Alice"}'); // โ unquoted key
// โ
Valid JSON requires double quotes, no trailing commas
JSON.parse('{"name": "Alice"}'); // โ valid
The Robust Fetch Pattern
async function fetchJSON(url, options) {
const response = await fetch(url, options);
// Check status
if (!response.ok) {
const text = await response.text();
throw new Error(`HTTP ${response.status}: ${text.slice(0, 200)}`);
}
// Handle empty responses
const text = await response.text();
if (!text) return null;
// Parse safely with a helpful error
try {
return JSON.parse(text);
} catch (err) {
throw new Error(`Invalid JSON response: ${text.slice(0, 200)}`);
}
}
// Usage
try {
const data = await fetchJSON('/api/users');
} catch (err) {
console.error('Fetch failed:', err.message); // clear error, not cryptic
}
Debugging Steps
// 1. Log the raw response text to see what you actually got
const response = await fetch(url);
const text = await response.text();
console.log('Raw response:', text); // reveals HTML, empty, or malformed JSON
// 2. Check the Network tab in DevTools
// - Status code (200? 404? 500?)
// - Content-Type header (application/json?)
// - Response body (is it actually JSON?)
// 3. Test the endpoint directly
curl -i https://api.example.com/data
// See the exact status, headers, and body
Frequently Asked Questions
Q: Why does the error mention "<" or "<!DOCTYPE"?
A: Because the response starts with HTML, not JSON. You got an HTML page (usually a 404 or 500 error page, or your SPA's index.html) instead of the expected JSON. The "<" is the first character of the HTML tag, which isn't valid JSON.
Q: Why does it work sometimes but not others?
A: It fails when the server returns non-JSON โ an error page (404/500), an empty body, or a redirect to HTML. It works when the API returns proper JSON. The intermittent failure often means the API is sometimes erroring and returning HTML error pages.
Q: How do I prevent this error?
A: Always check response.ok and the content-type before calling response.json(), handle empty bodies, and wrap parsing in try/catch. Use a robust fetch wrapper (shown above) that gives clear errors instead of the cryptic "unexpected token" message.
Q: I'm getting my app's HTML instead of API data. Why?
A: Your API URL is likely wrong or the endpoint doesn't exist, so the server falls back to serving your SPA's index.html (which starts with <!DOCTYPE). Verify the API path is correct and the endpoint is registered. Check the Network tab to see the actual response.
Q: Should I use response.json() or response.text() then JSON.parse()?
A: response.json() is convenient but throws a cryptic error on invalid JSON. Using response.text() then JSON.parse() in a try/catch lets you inspect the raw response and give a clear error message when it's not JSON โ better for debugging.
Conclusion
"SyntaxError: Unexpected token in JSON at position 0" means you tried to parse non-JSON โ almost always an HTML error page (starts with "<") or an empty response. The fix: check response.ok and the content-type header before parsing, handle empty bodies, and wrap JSON.parse in try/catch. The "<" clue tells you the response is HTML, so investigate why โ usually a wrong URL, a server error returning an HTML error page, or an SPA fallback. Use a robust fetch wrapper that logs the raw response and gives clear errors. Once you check the response before parsing it, this cryptic error becomes an informative one that points you straight to the real problem.
๐ You might also like
๐ Share this article




โ๏ธ Leave a Comment