Fix: SyntaxError: Unexpected token < in JSON at position 0
The Error
You call JSON.parse() or response.json() and get one of these errors:
Chrome / Node.js:
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
Older Chrome / Node versions:
SyntaxError: Unexpected token < in JSON at position 0
Firefox:
JSON.parse: unexpected character at line 1 column 1 of the JSON data
Empty response variant:
SyntaxError: Unexpected end of JSON input
Other token variants:
SyntaxError: Unexpected token 'u', "undefined" is not valid JSON
SyntaxError: Unexpected token 'N', "Not Found" is not valid JSON
SyntaxError: Expected property name or '}' in JSON at position 1
All of these mean the same thing: the string you passed to JSON.parse() (or that response.json() tried to parse) is not valid JSON. The token mentioned in the error (<, u, N, etc.) tells you what character the parser hit when it was expecting valid JSON.
Why This Happens
JSON.parse() expects a string that is strictly valid JSON. JSON is not JavaScript — it has its own, stricter syntax rules. When the parser encounters something that violates those rules, it throws a SyntaxError.
The most common scenario: your fetch() call gets back an HTML page instead of JSON. That HTML starts with <!DOCTYPE html> or <html>, so the first character the JSON parser sees is <. This typically happens when:
- The API URL is wrong (404 page returned as HTML)
- The server threw an error (500 page returned as HTML)
- A proxy or CDN intercepted the request and returned its own HTML page
- You’re hitting the wrong server entirely
But it can also be a JSON syntax problem — trailing commas, single quotes, comments, or encoding issues in the JSON string itself.
Fix 1: Your API Is Returning HTML, Not JSON
This is the cause 90% of the time. Your fetch() call is getting back an HTML page instead of a JSON response.
Step 1: Check what you’re actually receiving. Replace .json() with .text() temporarily:
// Instead of this:
const data = await response.json();
// Do this temporarily to debug:
const text = await response.text();
console.log(text);
If you see HTML (like <!DOCTYPE html> or an error page), the problem is not with JSON parsing — it’s with what the server is sending back.
Step 2: Figure out why you’re getting HTML. Open the Network tab in DevTools, find the request, and check:
- Status code: A
404means the URL is wrong. A500means the server crashed. A200with HTML means the server is returning a page instead of an API response. - Response body: Click the response to see what came back.
- Request URL: Verify it matches what you expected.
Step 3: Fix the URL. Common mistakes:
// Wrong — relative URL hits your frontend server, not your API
fetch('/api/users')
// Wrong — missing protocol
fetch('api.example.com/users')
// Wrong — typo in path
fetch('http://localhost:3000/api/uers')
// Correct
fetch('http://localhost:5000/api/users')
If you’re using a relative URL like /api/users and your frontend is served by a dev server (Vite, Next.js, CRA), the request goes to the frontend server, which returns its HTML page for any unrecognized route. Either use the full API URL or set up a dev proxy.
Fix 2: Check the Response Status Before Parsing
Never call .json() without checking if the response was successful first. A non-2xx response often returns HTML error pages.
const response = await fetch('/api/users');
if (!response.ok) {
// Don't try to parse — the response might be HTML
const text = await response.text();
throw new Error(`HTTP ${response.status}: ${text}`);
}
const data = await response.json();
With axios, the pattern is different because axios throws on non-2xx status codes automatically:
try {
const { data } = await axios.get('/api/users');
} catch (error) {
if (error.response) {
console.error('Status:', error.response.status);
console.error('Body:', error.response.data);
} else {
console.error('Network error:', error.message);
}
}
Fix 3: Your Server Is Not Setting Content-Type Correctly
Sometimes the server returns JSON data, but without the correct Content-Type: application/json header. Some frameworks and proxies behave differently based on this header.
Make sure your server sets the Content-Type header:
Express / Node.js:
// res.json() sets Content-Type automatically
res.json({ message: 'ok' });
// If using res.send() or res.end(), set it manually
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ message: 'ok' }));
Python / Flask:
from flask import jsonify
@app.route('/api/data')
def get_data():
return jsonify({"message": "ok"}) # Sets Content-Type automatically
Django:
from django.http import JsonResponse
def get_data(request):
return JsonResponse({"message": "ok"}) # Sets Content-Type automatically
Fix 4: Invalid JSON Syntax in Static Files
If you’re parsing a JSON file (like package.json, tsconfig.json, or a config file) and hitting this error, the file contains invalid JSON syntax. JSON is stricter than JavaScript objects. Note that YAML config files have their own syntax pitfalls — see Fix: YAML mapping values not allowed here for similar issues in YAML.
Trailing commas are not allowed:
// INVALID
{
"name": "my-app",
"version": "1.0.0",
}
// VALID
{
"name": "my-app",
"version": "1.0.0"
}
Single quotes are not allowed:
// INVALID
{
'name': 'my-app'
}
// VALID
{
"name": "my-app"
}
Comments are not allowed:
// INVALID
{
// This is a comment
"name": "my-app"
}
// VALID
{
"name": "my-app"
}
Note: tsconfig.json is a special case — TypeScript’s parser supports comments and trailing commas (it uses JSONC, not strict JSON). But if you read tsconfig.json with JSON.parse() or fs.readFileSync + JSON.parse, it will fail. Use TypeScript’s own API or a JSONC parser like jsonc-parser instead.
Unquoted keys are not allowed:
// INVALID
{
name: "my-app"
}
// VALID
{
"name": "my-app"
}
Use a JSON validator like jsonlint.com or your editor’s built-in JSON validation to find the exact syntax error. VS Code highlights JSON syntax errors with red squiggly lines.
Fix 5: Empty Response Body
If you get Unexpected end of JSON input, the response body is empty. Calling response.json() on an empty body fails because JSON.parse("") throws.
Common causes:
- A
204 No Contentresponse (which correctly has no body) - A
DELETEorPUTendpoint that returns an empty response - A server error that returns a blank page
This can also happen when your API endpoint is behind a reverse proxy like Nginx that returns a 502 Bad Gateway with an empty body.
Fix — check for empty bodies before parsing:
const response = await fetch('/api/resource', { method: 'DELETE' });
if (response.status === 204 || response.headers.get('content-length') === '0') {
// No body to parse
return null;
}
const data = await response.json();
Or use a safe wrapper:
async function safeJson(response) {
const text = await response.text();
if (!text) return null;
return JSON.parse(text);
}
Fix 6: CORS Proxy Returning HTML
If you’re using a CORS proxy (like cors-anywhere or a similar service), the proxy itself may be returning an HTML error page instead of forwarding your request. This happens when:
- The proxy is rate-limited and returns a “too many requests” HTML page
- The proxy URL format is wrong
- The proxy service is down
Debug it by logging the raw response:
const response = await fetch('https://cors-proxy.example.com/https://api.example.com/data');
const text = await response.text();
console.log('Proxy response:', text);
If you see the proxy’s HTML page, you need a different proxy or a proper backend solution. Public CORS proxies are unreliable for production. See setting up proper CORS headers instead.
Fix 7: BOM Characters in JSON Files
A BOM (Byte Order Mark) is an invisible Unicode character (U+FEFF) that some text editors add at the beginning of files. It’s invisible in most editors, but JSON.parse() sees it and throws because \uFEFF is not valid JSON.
Check for a BOM:
const text = await response.text();
console.log('First char code:', text.charCodeAt(0)); // 65279 = BOM
Fix — strip the BOM before parsing:
function stripBom(text) {
return text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
}
const data = JSON.parse(stripBom(text));
If the BOM is in a static file, re-save the file as UTF-8 without BOM. In VS Code, click the encoding in the status bar (bottom right), select “Save with Encoding,” then choose “UTF-8.”
Fix 8: Parsing a Non-String Value
JSON.parse() expects a string. If you pass something else, JavaScript coerces it to a string first, which usually produces invalid JSON.
JSON.parse(undefined); // SyntaxError: "undefined" is not valid JSON
JSON.parse(null); // This actually works — returns null
JSON.parse({}); // SyntaxError: "[object Object]" is not valid JSON
JSON.parse(someBuffer); // Might work, might not, depending on encoding
Fix — make sure you’re passing a string:
// If working with a Buffer (Node.js)
const data = JSON.parse(buffer.toString('utf-8'));
// If the value might be undefined
if (typeof text === 'string' && text.length > 0) {
const data = JSON.parse(text);
}
Fix 9: Double Serialization
Sometimes data gets JSON.stringify()-ed twice. The result is a valid JSON string containing an escaped JSON string, and parsing it once gives you a string instead of an object:
const obj = { name: 'Alice' };
const double = JSON.stringify(JSON.stringify(obj));
// '"{\\"name\\":\\"Alice\\"}"'
JSON.parse(double); // '{"name":"Alice"}' — a string, not an object
JSON.parse(JSON.parse(double)); // { name: 'Alice' } — works, but you shouldn't need this
If you find yourself calling JSON.parse() twice, the real fix is to find where the double serialization happens and remove one of the JSON.stringify() calls. Common culprits:
- Storing already-serialized JSON in a database column, then serializing the whole row again
- An API middleware that calls
JSON.stringify()on a response body that’s already a string localStorage.setItem('data', JSON.stringify(JSON.stringify(obj)))— an accidental double wrap
Fix 10: Parsing a Response You Already Read
The body of a Response object can only be consumed once. If you call .json() or .text() twice, the second call fails.
const response = await fetch('/api/data');
const text = await response.text(); // reads the body
const data = await response.json(); // TypeError: body used already
Fix — clone the response if you need to read it twice:
const response = await fetch('/api/data');
const cloned = response.clone();
const text = await cloned.text();
console.log('Raw:', text);
const data = await response.json();
Or parse the text yourself:
const response = await fetch('/api/data');
const text = await response.text();
console.log('Raw:', text);
const data = JSON.parse(text);
Still Not Working?
-
Check the exact URL in the Network tab. Your code might construct the URL correctly, but a service worker, browser extension, or redirect could be changing it. Look at the actual request URL the browser sent, not the one in your code.
-
Your API route might not exist in production. If it works in development but fails in production, your backend may not be deployed, the URL may be different, or your frontend server is serving its
index.htmlas a catch-all for unknown routes (common with single-page app hosting on Netlify, Vercel, etc.). -
Hidden characters in the response. Beyond BOM characters, some APIs return invisible Unicode characters (zero-width spaces, soft hyphens) that break parsing. Inspect the raw bytes:
const text = await response.text(); console.log([...text].map(c => c.charCodeAt(0))); -
The response is gzipped but not decoded. In Node.js, if you’re using
http.get()directly (notfetchoraxios), the response might be compressed. You need to decompress it before parsing. Usefetchoraxiosinstead — they handle decompression automatically. -
Your server returns different Content-Type based on Accept header. Some APIs return HTML when the
Acceptheader isn’t set toapplication/json. Set it explicitly:fetch('/api/data', { headers: { 'Accept': 'application/json' } }); -
Your ESLint config is pointing to a corrupted JSON file. If the error happens during a build step and the stack trace mentions a config file like
.eslintrc.jsonorbabel.config.json, open that file and validate its syntax. -
The JSON is truncated. If a large response is cut off mid-stream (network timeout, proxy buffer limit), you get
Unexpected end of JSON inputbecause the JSON is incomplete. Check theContent-Lengthheader against the actual body length. If they don’t match, there’s a network or proxy issue. -
You’re running
JSON.parse()on a JavaScript object that’s already parsed. This happens when a library (like axios) has already parsed the response for you. Check the type before parsing:// axios already parses JSON responses const { data } = await axios.get('/api/users'); // data is already an object — don't call JSON.parse(data) console.log(typeof data); // 'object', not 'string' -
Your localhost server isn’t running. If the API server is down, your request might hit the frontend dev server instead, which returns HTML. Make sure your backend is running before making requests.
Related: If you’re getting CORS errors on the same API call, fix CORS first — a CORS proxy returning HTML is a common cause of JSON parse errors. If the error involves accessing a property on the parsed result and you get Cannot read properties of undefined, the JSON parsed successfully but has a different structure than your code expects.
Related Articles
Fix: ESLint Parsing error: Unexpected token (JSX, TypeScript, ES modules)
How to fix ESLint 'Parsing error: Unexpected token' for JSX, TypeScript, and ES module syntax by configuring the correct parser, parserOptions, and ESLint config format.
Fix: CORS preflight request blocked — Response to preflight does not have HTTP ok status
How to fix 'Response to preflight request doesn't pass access control check' and 'preflight channel did not succeed' CORS errors by handling OPTIONS requests, setting correct headers, and configuring your server.
Fix: Module parse failed: Unexpected token (Webpack / Vite / esbuild)
How to fix 'Module parse failed: Unexpected token' in Webpack, Vite, and esbuild by configuring the correct loaders and transforms for JSX, TypeScript, CSS, JSON, and other file types.
Fix: TS2322 Type 'X' is not assignable to type 'Y'
How to fix TypeScript error TS2322 'Type is not assignable to type'. Covers literal types vs general types, string vs String, union types, interface compatibility, generic constraints, readonly arrays, excess property checking, discriminated unions, type assertions, type widening and narrowing, React event handlers, Promise return types, and enum mismatches.