This error occurs when JavaScript attempts to parse HTML as JSON, typically happening when a fetch request receives an HTML error page (like a 404) instead of the expected JSON response.
The "Unexpected token <" error is a clear indicator that your code tried to parse HTML as JSON. The "<" character is the first character of HTML tags like `<!DOCTYPE html>` or `<html>`, which are not valid JSON. This error most commonly occurs in React applications when using `fetch()` or `axios` to make API requests. When you call `.json()` on a response, JavaScript expects valid JSON data. If the server returns HTML instead (such as a 404 error page, 500 error page, or even your React app's index.html), the JSON parser encounters the opening angle bracket and throws this error. The error happens at "position 0" because the parser fails immediately upon encountering the first character of the response.
Open your browser's Developer Tools (F12 or right-click → Inspect) and go to the Network tab. Find the failed request and check the Response tab to see what the server actually returned.
// If you see HTML starting with <!DOCTYPE html> or <html>,
// then you're getting an HTML page instead of JSONLook for status codes like 404 (Not Found) or 500 (Internal Server Error) in the Status column.
Modify your fetch code to log the raw response before parsing it as JSON. This helps identify what you're actually receiving.
fetch('/api/users')
.then(response => {
// Log the raw response
return response.text().then(text => {
console.log('Raw response:', text);
try {
return JSON.parse(text);
} catch (err) {
console.error('Failed to parse JSON:', err);
throw err;
}
});
})
.then(data => console.log(data))
.catch(error => console.error('Error:', error));Check your console to see the actual response content.
Double-check that your API URL matches the backend route exactly, including spelling, case sensitivity, and path structure.
// ❌ Wrong - typo in endpoint
fetch('/api/usres')
// ✅ Correct
fetch('/api/users')
// ❌ Wrong - missing /api prefix
fetch('/users')
// ✅ Correct - with proper prefix
fetch('/api/users')If using environment variables, verify they're set correctly:
// .env.local
REACT_APP_API_URL=http://localhost:3001
// In your code
const apiUrl = process.env.REACT_APP_API_URL;
console.log('API URL:', apiUrl); // Verify it's not undefined
fetch(`${apiUrl}/api/users`)If your backend server isn't running, the frontend may receive an error page from the development server instead.
# Start your backend server
cd backend
npm start
# Or if using a monorepo
npm run dev:serverAfter starting the server, verify it's accessible:
# Test with curl
curl http://localhost:3001/api/users
# Should return JSON, not HTMLIf you're using Create React App with a backend API, configure the proxy correctly in package.json.
// package.json
{
"name": "my-app",
"proxy": "http://localhost:3001"
}After adding or modifying the proxy, restart the development server:
# Stop the server (Ctrl+C) and restart
npm startThen use relative URLs in your fetch calls:
// ✅ Use relative URL - proxy will forward to backend
fetch('/api/users')
.then(res => res.json())
.then(data => console.log(data));Implement defensive coding by checking the response status and content-type before parsing.
async function fetchData(url) {
const response = await fetch(url);
// Check if response is OK (status 200-299)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Check content-type header
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
const text = await response.text();
throw new Error(`Expected JSON but received: ${text.substring(0, 100)}`);
}
return response.json();
}
// Usage
fetchData('/api/users')
.then(data => console.log(data))
.catch(error => console.error('Fetch error:', error.message));This prevents the confusing "Unexpected token" error and gives you clearer error messages.
Ensure your backend routes are properly defined and return JSON responses.
// Express.js example
const express = require('express');
const app = express();
// ✅ Correct - returns JSON
app.get('/api/users', (req, res) => {
res.json({ users: [] });
});
// ❌ Wrong - might return HTML on error
app.get('/api/data', (req, res) => {
const data = getData(); // If this throws, Express sends HTML error
res.send(data);
});
// ✅ Better - handle errors and return JSON
app.get('/api/data', async (req, res) => {
try {
const data = await getData();
res.json(data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(3001, () => {
console.log('Server running on port 3001');
});After modifying routes, restart your backend server.
In production, ensure your API base URL points to the correct backend server.
// config.js
const API_BASE_URL = process.env.NODE_ENV === 'production'
? 'https://api.yourdomain.com'
: 'http://localhost:3001';
export default API_BASE_URL;
// In your components
import API_BASE_URL from './config';
fetch(`${API_BASE_URL}/api/users`)
.then(res => res.json())
.then(data => console.log(data));For Vite, use environment files:
# .env.production
VITE_API_URL=https://api.yourdomain.com// In your code
const apiUrl = import.meta.env.VITE_API_URL;
fetch(`${apiUrl}/api/users`)Understanding the Root Cause: This error fundamentally stems from a mismatch between what your code expects (JSON) and what it receives (HTML). In most web servers, when a request fails, the default behavior is to return an HTML error page. This is why 404 and 500 errors commonly trigger this issue.
Static Hosting Gotcha: When deploying React apps to static hosts (Netlify, Vercel, GitHub Pages), configure redirects properly. Otherwise, API routes that don't exist will return your app's index.html, causing this error. For example, in Netlify, create a _redirects file:
/api/* https://your-backend.com/api/:splat 200
/* /index.html 200CORS and Proxy Issues: If you see this error only in production but not locally, it might be a CORS issue. When CORS blocks a request, the browser may return an error page instead of the API response. Check your browser console for CORS-related errors. Your backend should include proper CORS headers:
// Express.js CORS setup
const cors = require('cors');
app.use(cors({
origin: 'https://yourfrontend.com'
}));Response.text() vs Response.json(): The response.json() method is convenient but fails silently when the response isn't JSON. For debugging, use response.text() first to see raw content, then manually parse:
const response = await fetch('/api/data');
const text = await response.text();
console.log('Raw:', text);
const data = JSON.parse(text); // Now you see the exact parsing errorDevelopment vs Production: In Create React App development mode, the dev server proxies requests to your backend. In production, there's no proxy—your app makes direct requests to wherever it's deployed. This is why environment-specific configuration is crucial.
API Versioning: Consider versioning your API routes (e.g., /api/v1/users) to make endpoint changes clearer and reduce typo-related issues.
Prop spreading could cause security issues
Prop spreading could cause security issues
Error: error:0308010C:digital envelope routines::unsupported
Error: error:0308010C:digital envelope routines::unsupported
React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render.
React Hook useEffect placed inside a condition
Hook can only be called inside the body of a function component
Hook can only be called inside the body of a function component
Rollup failed to resolve import during build
How to fix "Rollup failed to resolve import" in React