Why JSON Breaks in Fetch Responses — And How to Fix It
The fetch API handles JSON parsing differently than you might expect. Here are the most common reasons JSON fails in fetch calls and the defensive patterns to prevent it.
The fundamental mistake — not checking response.ok
The fetch API does not throw an error for HTTP error responses like 404 or 500. It only throws for network failures. This means calling response.json() on an error response tries to parse HTML error pages as JSON.
// ❌ Wrong — no status check
const response = await fetch('/api/users')
const data = await response.json()
// Throws if server returns HTML error page!// ✅ Correct — always check response.ok
const response = await fetch('/api/users')
if (!response.ok) {
const errorText = await response.text()
throw new Error(`HTTP ${response.status}: ${errorText}`)
}
const data = await response.json()The empty response problem
Some API endpoints return 204 No Content or an empty body. Calling response.json() on an empty body throws a parse error.
// Handle empty responses safely
async function fetchJson(url) {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
// Check if there's actually content
const contentLength = response.headers.get('content-length')
const contentType = response.headers.get('content-type')
if (
response.status === 204 ||
contentLength === '0' ||
!contentType?.includes('application/json')
) {
return null
}
return response.json()
}CORS errors producing invalid JSON
When a CORS error occurs the response body is often empty or contains a CORS error message — not JSON. The error appears as a JSON parse failure but the real issue is CORS.
// Detect CORS vs JSON issues
try {
const response = await fetch(url)
const data = await response.json()
} catch (e) {
if (e instanceof TypeError && e.message.includes('Failed to fetch')) {
console.error('Network or CORS error — not a JSON error')
// Check browser console for CORS details
} else if (e instanceof SyntaxError) {
console.error('JSON parse error — check response body')
}
}The defensive fetch wrapper
Instead of writing error handling every time create a reusable fetch wrapper that handles all JSON edge cases:
async function apiFetch(url, options = {}) {
let response
try {
response = await fetch(url, {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
...options.headers,
},
...options,
})
} catch (networkError) {
throw new Error('Network error: ' + networkError.message)
}
// Get response text first
const text = await response.text()
// Try to parse as JSON
let data = null
if (text) {
try {
data = JSON.parse(text)
} catch {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${text.slice(0, 100)}`)
}
throw new Error('Invalid JSON response: ' + text.slice(0, 100))
}
}
if (!response.ok) {
throw new Error(data?.message ?? `HTTP ${response.status}`)
}
return data
}Validate and format JSON instantly
Paste any JSON response to check its validity and fix common errors.
Open JSON Formatter →