React throws this parser error when it tries to JSON.parse() a response that is actually HTML, which usually happens because the front-end asset server (webpack dev server or CRA proxy) answers the request instead of your API.
JavaScript reports this error when JSON.parse() or response.json() sees a "<" character instead of one of the permitted JSON tokens. In React apps this almost always means the HTTP response was an HTML page—often the index.html served by the webpack dev server or a 404/500 error page—so the parser aborts before it can deserialize the JSON you expected.
If you call fetch() or $.ajax().then(r => r.json()), the parser error hides the HTML payload. Log the response.text() (or xhr.responseText) to prove it is markup before anything else runs:
fetch('/api/threads')
.then(async (response) => {
const text = await response.text();
console.warn('Response text:', text);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return JSON.parse(text);
})
.then((data) => setThreads(data))
.catch((error) => console.error('JSON parse failed', error));If the console shows <!doctype html> or your React app shell, the parser error is masking an HTML response.
Check the browser's Network tab and make sure the “Request URL” matches the port where your API is running (e.g. http://localhost:3001/api/threads). React running on :3000 will hit http://localhost:3000 unless you:
- Pass an absolute URL from props or environment variables
- Configure proxy in CRA's package.json
- Use setupProxy.js to forward /api to your backend
- Use process.env.NEXT_PUBLIC_API_BASE_URL in Next.js
If the request is landing on /?=... (the webpack dev server refresh endpoint) instead of /api/threads, configure your URL or proxy so the backend returns JSON instead of HTML.
In Create React App or a webpack dev server, the proxy should forward the path rather than serving index.html. Example setupProxy.js:
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function (app) {
app.use(
'/api',
createProxyMiddleware({
target: 'http://localhost:3001',
changeOrigin: true,
pathRewrite: {
'^/api': '/api'
},
onProxyReq(proxyReq) {
proxyReq.setHeader('Accept', 'application/json');
}
})
);
};If you rely on Next.js rewrites, ensure they route to the API to avoid hitting the React shell.
Only parse JSON when the response is successful and reports the right content-type:
const response = await fetch('/api/threads');
if (!response.ok) {
const body = await response.text();
throw new Error(`API error: ${response.status} - ${body}`);
}
const contentType = response.headers.get('content-type') || '';
if (!contentType.includes('application/json')) {
throw new Error(`Expected JSON but got ${contentType}`);
}
const data = await response.json();That way you can early-exit when the server returns HTML and log the exact payload rather than letting JSON.parse throw.
If you control the API, send JSON even for errors and set the header explicitly:
app.get('/api/threads', (req, res) => {
try {
res.json({ threads: getThreads() });
} catch (error) {
res.status(500).json({ error: 'server failure', details: error.message });
}
});Avoid rendering HTML error templates for AJAX endpoints. If Express hits a route that does not exist, configure a JSON fallback to prevent the React client from sniffing markup.
Webpack dev server nuance: When running React on :3000 and Express on :3001, CRA’s proxy or webpackDevServer proxy must forward /api requests to the backend instead of replying with the SPA shell. In addition to the proxy shown above, you can also hardcode the backend base URL as an environment variable like REACT_APP_API_URL=http://localhost:3001/api and call fetch(${process.env.REACT_APP_API_URL}/threads).
Debugging tips: Look in the Network tab’s “Response” panel for the failing call. If you see markup, copy it into the console and run JSON.parse(text) to confirm the parser will fail with the same message. Some CDNs or middleware rebroadcast an HTML login page (with <form> etc.) when authentication fails; that is what the unexpected < denotes.
React Hook useCallback has a missing dependency: 'variable'. Either include it or remove the dependency array react-hooks/exhaustive-deps
React Hook useCallback has a missing dependency
Cannot use private fields in class components without TS support
Cannot use private fields in class components without TS support
Cannot destructure property 'xxx' of 'undefined'
Cannot destructure property of undefined when accessing props
useNavigate() may be used only in the context of a <Router> component.
useNavigate() may be used only in the context of a Router component
Cannot find module or its corresponding type declarations
How to fix "Cannot find module or type declarations" in Vite