Fix Fetch JSON Parse Errors Caused by Non-JSON Responses

Summary

During an investigation of intermittent failures when calling a REST endpoint with fetch(), the root cause was that the server sometimes returned non‑JSON content types (e.g., text/html, text/plain) due to server‑side errors. The client, always calling .json(), attempted to parse these responses and threw a SyntaxError.


Root Cause

  • Server‑side intermittent errors
    • The API sometimes responds with an HTML error page instead of JSON.
    • The Content-Type header is not guaranteed to be application/json for error responses.
  • Client assumptions
    • The code always executes response.json() without checking response.ok or the Content-Type.
    • Unchecked Promise rejections propagate as uncaught errors.

Why This Happens in Real Systems

  • Load balancers and micro‑service orchestrators may return fallback pages if downstream services timeout or crash.
  • API gateways can emit policy‑related error messages that are HTML or plain text.
  • Network hiccups can trigger incomplete responses that the browser interprets as HTML.

Real-World Impact

  • Unreliable UX: Users see errors sporadically with no explanation.
  • Poor debugging: Stack traces point to SyntaxError: Unexpected token < or similar, hiding the true cause.
  • Increased support tickets: Clients believe the API is down, while the real issue is in error handling logic.

Example or Code (if necessary and relevant)

fetch('https://api.example.com/data')
  .then(response => {
    if (!response.ok) {
      return response.text().then(text => Promise.reject(new Error(text)));
    }
    const contentType = response.headers.get('content-type');
    if (contentType && contentType.includes('application/json')) {
      return response.json();
    }
    return Promise.reject(new Error('Expected application/json but got ' + contentType));
  })
  .then(data => {
    console.log('Data received:', data);
  })
  .catch(error => {
    console.error('Error:', error.message);
  });

How Senior Engineers Fix It

  • Validate the response
    • Check response.ok to detect HTTP errors before parsing.
    • Inspect the Content‑Type header and only parse if it contains application/json.
  • Graceful fallbacks
    • Provide default structures or user‑friendly error messages when parsing fails.
    • Log detailed diagnostics (status code, headers, raw body) to aid ops.
  • Automated tests
    • Mock error responses with different content types to ensure the client behaves correctly.
    • Include edge cases like broken JSON, empty responses, and large payloads.
  • Infrastructure monitoring
    • Gateways: ensure proper fault‑tolerant handling or redirection to JSON responses.
    • Alert on unexpected content types or high error rates.

Why Juniors Miss It

  • They assume fetch() always receives JSON when the status code is 200.
  • They overlook HTTP error status codes and ok property.
  • They ignore the importance of Content-Type validation.
  • They lack experience handling promises and rejections cleanly.

By following the practices above, you eliminate the ambiguity and ensure robust, predictable data fetching in production.

Leave a Comment