Fix 'Unexpected token' JSON parse error in fetch API
This error means fetch got non-JSON data back. Start by checking the response body, then fix server headers or handle edge cases.
What's actually happening here
The error SyntaxError: Unexpected token when calling response.json() means the server returned something that isn't valid JSON. JavaScript's JSON parser expects a specific format — objects wrapped in {}, arrays in [], strings in double quotes. If it sees a plain text string, an HTML page, or even a network error message, it throws this error.
I've seen this in three common scenarios: (1) the endpoint returns a 404 or 500 with an HTML error page, (2) the response is empty (status 204 No Content), or (3) the server sends JSON but without the correct Content-Type header and something in your stack transforms it. Let's fix it step by step.
Fix 1: Check what the server actually sends (30 seconds)
The fastest way to debug is to log the raw response before parsing. Replace response.json() with response.text() and print it to the console.
fetch('https://api.example.com/data')
.then(response => response.text())
.then(text => {
console.log('Raw response:', text);
// Then try parsing manually
try {
const data = JSON.parse(text);
console.log('Parsed:', data);
} catch (err) {
console.error('Parse failed:', err.message);
}
})
.catch(err => console.error('Fetch error:', err));
What you'll see in the console tells you everything. If text contains <html>, you're hitting a server error page. If it's empty or OK, the endpoint returned a non-JSON success response. If it's a string like "hello", that's valid JSON but the error message is misleading — your real problem is earlier in the pipeline.
Fix 2: Ensure the server returns proper JSON (5 minutes)
If you control the backend, make sure the response is explicitly JSON. In Express.js, for example, use res.json() instead of res.send() or res.send({}). The difference: res.json() sets the Content-Type: application/json header and stringifies the object. res.send() might send a stringified version but can also send raw strings or buffers.
// Express.js — correct way
app.get('/api/data', (req, res) => {
const data = { name: 'Yuki' };
res.json(data); // This sets Content-Type and sends JSON
});
// What NOT to do
app.get('/api/data', (req, res) => {
const data = { name: 'Yuki' };
res.send(data); // Express guesses content type — might send as JSON, but not reliable
});
If you're using a framework like Django, return JsonResponse(data). For Rails, render json: data. For PHP, header('Content-Type: application/json'); echo json_encode($data);.
Also check if a reverse proxy (nginx, Apache) or CDN is modifying the response. Some proxies compress or cache the result and change headers. I once spent an hour debugging a case where Cloudflare was serving a gzipped HTML error page for a 502.
Fix 3: Handle HTTP error statuses explicitly (5 minutes)
By default, fetch does not reject on HTTP errors like 404 or 500. It only rejects on network failures. So if your API returns a 404 with an HTML body, response.ok is false, but response.json() still tries to parse the HTML.
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
// Throw a custom error with status and statusText
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(err => console.error('Failed:', err.message));
Why this matters: The error message changes from the cryptic Unexpected token to something like HTTP 404: Not Found. That tells you the endpoint URL is wrong or the server is misconfigured.
Fix 4: Handle empty responses (5 minutes)
Some APIs return 204 No Content with an empty body. Calling .json() on that throws Unexpected token because there's nothing to parse. You need to check the status before parsing.
fetch('https://api.example.com/delete/123', { method: 'DELETE' })
.then(response => {
if (response.status === 204) {
return null; // No content — return null explicitly
}
return response.json();
})
.then(data => {
if (data === null) {
console.log('Deleted successfully, no content returned');
} else {
console.log('Response data:', data);
}
});
Same applies for 201 Created endpoints that might return an empty body. Always check status codes.
Fix 5: Validate JSON before parsing client-side (15+ minutes)
If you don't control the server or the response is unreliable, add client-side validation. Use a try-catch around the JSON parse and fall back to text if it fails.
async function safeFetchJson(url, options = {}) {
try {
const response = await fetch(url, options);
const text = await response.text();
// Quick string-based check: does it start with { or [ after trimming?
const trimmed = text.trim();
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
return JSON.parse(trimmed);
}
// If it looks like a plain string, return it wrapped
return { raw: trimmed };
} catch (err) {
console.warn('safeFetchJson: failed to parse, returning raw text', err.message);
return { error: err.message };
}
}
// Usage
const data = await safeFetchJson('/api/data');
console.log(data);
Don't do this blindly — you're masking the underlying issue. Use it as a temporary measure while you fix the server. The real fix is always on the server side if you control it.
BONUS: jQuery AJAX gotcha
If you're using jQuery's $.ajax and getting SyntaxError: Unexpected token, the cause is similar but the fix differs. jQuery by default tries to parse the response based on dataType. Set dataType: 'json' and check the error callback.
$.ajax({
url: '/api/data',
dataType: 'json',
success: function(data) {
console.log(data);
},
error: function(jqXHR, textStatus, errorThrown) {
console.log('Status:', jqXHR.status);
console.log('Response text:', jqXHR.responseText);
console.log('Error:', errorThrown);
}
});
The jqXHR.responseText will show you the raw HTML or error message that caused the parse failure. Same logic applies — the server sent something that isn't JSON.
The one thing everyone misses
There's a subtle case where the server sends Content-Type: application/json but the body is actually HTML. This happens when a reverse proxy or middleware re-writes the response body without updating the header. I've seen this with AWS ALB and custom error pages. The response header says JSON, but the body is an HTML error page. Your browser trusts the header, not the body. The JSON parser errors because it sees <!DOCTYPE html>.
To catch this, you need to parse the response manually and ignore the Content-Type header. Use response.text() always, then test if it's JSON with a try-catch.
Summary checklist
- Log raw response with
response.text()— see what you're actually getting - Check server status code — handle 4xx/5xx before parsing
- Ensure server sends valid JSON with correct
Content-Type - Handle empty responses (204, 201) explicitly
- If all else fails, wrap the parse in a try-catch and log the raw text
That's it. Most of the time, the fix is in step 1 or 2. If you're still stuck after step 5, the problem is almost certainly on the server — the endpoint doesn't exist, returns an error page, or is protected by auth and redirects to a login page.
Was this solution helpful?