Fixing 'Unexpected token < in JSON at position 0'
You call response.json() and your app crashes with SyntaxError: Unexpected token '<' in JSON at position 0. The < character tells you exactly what happened: your code expected JSON but received HTML instead.
This JSON parse error appears constantly in modern frontend stacks—browser fetch, Node.js, Next.js API routes, and serverless functions. Understanding why it happens and how to debug it quickly will save you hours of frustration.
Key Takeaways
- The “Unexpected token <” error means you’re parsing HTML as JSON—the
<is typically from<!DOCTYPE html>or an HTML tag. - Common causes include wrong URLs, authentication redirects, server errors returning HTML pages, and missing Content-Type headers.
- Debug by checking the HTTP status code first, then the Content-Type header, then the raw response body using
response.text(). - Build defensive fetch wrappers that validate responses before parsing to catch these issues with clear error messages.
Why Your API Returns HTML Instead of JSON
The error means JSON.parse() encountered an HTML document where it expected valid JSON. The < at position 0 is typically the opening character of <!DOCTYPE html> or an HTML tag.
Several scenarios cause this Content-Type mismatch:
Wrong or misspelled endpoint URLs. A typo in your fetch URL returns a 404 page—which is HTML, not JSON.
Authentication redirects. Expired tokens or missing auth headers trigger redirects to login pages. Your fetch receives the login page HTML.
Server errors returning HTML error pages. A 500 error from your API gateway or cloud provider often returns a styled HTML error page rather than a JSON error response.
Dev servers serving fallback HTML for unknown routes. Many SPAs return the HTML shell for unmatched paths, though some modern dev servers return structured error payloads.
Missing or incorrect Content-Type headers. Server code that forgets to set Content-Type: application/json may default to HTML.
A Practical Debugging Flow for Fetch API Errors
When JSON parsing fails, follow this sequence to identify the root cause:
Step 1: Check the HTTP Status Code
Before parsing, verify the response status. A 4xx or 5xx status often indicates the response won’t be JSON:
const response = await fetch('/api/data')
if (!response.ok) {
console.error(`HTTP ${response.status}: ${response.statusText}`)
const text = await response.text()
console.error('Response body:', text.substring(0, 200))
throw new Error(`Request failed with status ${response.status}`)
}
Step 2: Validate the Content-Type Header
Check what the server claims to be sending:
const contentType = response.headers.get('content-type')
if (!contentType || !contentType.includes('application/json')) {
const text = await response.text()
throw new Error(`Expected JSON, received: ${contentType}. Body: ${text.substring(0, 100)}`)
}
const data = await response.json()
Step 3: Log the Raw Response Body
When parsing fails, use response.text() instead of response.json() to see what you actually received:
async function fetchWithDebug(url) {
const response = await fetch(url)
const text = await response.text()
try {
return JSON.parse(text)
} catch (error) {
console.error('Failed to parse JSON. Raw response:', text.substring(0, 500))
throw error
}
}
Discover how at OpenReplay.com.
Common Real-World Pitfalls
Incorrect API base URLs in production. Environment variables pointing to wrong domains or missing trailing slashes cause 404s that return HTML.
API gateways and CDNs intercepting requests. Services like Cloudflare, AWS API Gateway, or Vercel may return their own HTML error pages for rate limits, timeouts, or misconfigurations.
Next.js App Router and middleware redirects. Middleware or auth redirects often return HTML, though some redirect paths emit small JSON payloads instead.
Server code missing JSON headers. Your API handler returns data but forgets to set the response content type:
// ❌ Missing Content-Type
res.send({ data: 'value' })
// ✅ Explicit JSON response
res.json({ data: 'value' })
CORS issues. Browsers block failed preflight requests before your code runs, but misconfigured servers or proxies may still return an HTML error page that your fetch call receives.
Defensive Fetch Pattern
Wrap your fetch calls with validation to catch these issues early:
async function safeFetch(url, options = {}) {
const response = await fetch(url, options)
if (!response.ok) {
const body = await response.text()
throw new Error(`HTTP ${response.status}: ${body.substring(0, 100)}`)
}
const contentType = response.headers.get('content-type')
if (!contentType?.includes('application/json')) {
const body = await response.text()
throw new Error(`Invalid content-type: ${contentType}`)
}
return response.json()
}
Conclusion
The “Unexpected token <” error always means you’re parsing HTML as JSON. Debug by checking the status code first, then the Content-Type header, then the raw response body. Most cases trace back to wrong URLs, auth redirects, or server errors returning HTML pages. Build defensive fetch wrappers that validate responses before parsing to catch these issues with clear error messages.
FAQs
Production environments often have different configurations. Check that your API base URL environment variable is set correctly, verify your authentication tokens are valid, and confirm that any proxies or CDNs between your frontend and API are configured properly. Production servers may also have stricter CORS policies that cause requests to fail.
Yes. Wrap your fetch calls in a defensive function that checks the response status and Content-Type header before calling response.json(). This lets you handle the error gracefully and display a meaningful message to users instead of crashing. Always validate responses before parsing them.
Use your browser's Network tab to inspect the actual response. If the response shows HTML content with a 404 or 500 status, the server is returning an error page. If the status is 200 but the content is HTML, check your server code to ensure it sets the correct Content-Type header and returns JSON.
The response.json() method attempts to parse the response body as JSON. If the body contains HTML or any non-JSON content, parsing fails. The response.text() method simply returns the raw response as a string without parsing, which is why it works regardless of content type. Use text() for debugging to see what you actually received.
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.