← All tutorials

JavaScript Fetch API: Handle JSON, HTTP Errors and Timeouts

Make fetch reliable: parse JSON, catch real network errors, check HTTP status and show clear UI states.

Diagram showing JavaScript fetch request and response lifecycle
Illustration for this tutorial. The examples and requests shown are educational.

The browser Fetch API feels simple until a request fails at the wrong time. A user might lose connection, the server might respond with HTTP 404, or an endpoint might return HTML instead of JSON. If you handle all of those cases the same way, it becomes difficult to show a useful message or debug the actual problem.

This guide builds a small JavaScript helper that handles timeouts, HTTP errors, malformed JSON and a clean loading state. It uses plain browser JavaScript rather than a framework so the behavior is easier to see.

1. Know what fetch resolves and rejects

A key detail: fetch() usually resolves when an HTTP response arrives, even if that response has status 404 or 500. It rejects for network failures or when the request is aborted. Always check response.ok rather than assuming a resolved Promise means success.

JavaScript · basic HTTP check
const response = await fetch('/api/profile');
if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);
}
const profile = await response.json();

2. Add a timeout and read the body once

To keep the interface responsive when requests hang, use an AbortController and a timer. Read the response body once as text, then parse it as JSON. This lets you create a clear error if the server returns a non-JSON page.

JavaScript · api.js
export async function getJson(url, { timeoutMs = 8000 } = {}) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(url, {
      signal: controller.signal,
      headers: { Accept: 'application/json' }
    });

    const raw = await response.text();
    let result;
    try {
      result = raw ? JSON.parse(raw) : null;
    } catch {
      throw new Error('The server returned invalid JSON');
    }

    if (!response.ok) {
      const serverMessage = result?.message || result?.error;
      throw new Error(serverMessage || `HTTP ${response.status}`);
    }

    return result;
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error('The request timed out');
    }
    throw error;
  } finally {
    clearTimeout(timer);
  }
}

This helper assumes that your API returns JSON. Empty successful responses such as HTTP 204 produce null. Authentication failures and unexpected HTML errors should be handled in a way that fits your actual backend contract.

Diagram of a browser request, HTTP status check and application rendering
Check both transport failures and application-level HTTP results.

3. Show the user loading, success and error states

The example below assumes your page has a status element and a button. Give users one visible place to look while an operation runs and a simple way to retry.

HTML · index.html
<button id="loadProfile" type="button">Load profile</button>
<p id="status" role="status" aria-live="polite"></p>
<pre id="result"></pre>
<script type="module" src="ui.js"></script>
JavaScript · ui.js
import { getJson } from './api.js';

const button = document.querySelector('#loadProfile');
const status = document.querySelector('#status');
const result = document.querySelector('#result');

button.addEventListener('click', async () => {
  button.disabled = true;
  status.textContent = 'Loading profile…';
  result.textContent = '';
  try {
    const data = await getJson('/api/profile.php?id=42');
    result.textContent = JSON.stringify(data, null, 2);
    status.textContent = 'Profile loaded.';
  } catch (error) {
    status.textContent = `Could not load profile: ${error.message}`;
  } finally {
    button.disabled = false;
  }
});

4. Handle API messages carefully

Use textContent when writing server-provided messages to a page. Avoid assigning untrusted strings to innerHTML, which can turn data into executable markup. Also, do not store server secrets or privileged API keys directly in browser JavaScript.

5. Understand CORS and local testing

If your HTML runs on one origin and the API runs on another, the server may need a CORS policy that allows the browser’s origin. You cannot fix a server-side CORS rejection by adding an arbitrary request header in the browser. Test the API separately with curl, check your browser Network tab, and make sure your local frontend is served over HTTP rather than opened as a file:// document when modules are involved.

6. Troubleshooting quick reference

SymptomWhat to check
fetch rejects immediatelyNetwork connection, DNS, mixed content, CORS, aborted request
HTTP 404 or 500Backend route and server logs; inspect response status
Unexpected token < in JSONServer likely returned an HTML page; inspect raw response
Loading never endsTimeout and finally cleanup, unresolved promises
Works in Postman but not browserCross-origin/browser restrictions and credentials settings

Frequently asked questions

Does fetch automatically reject when the server returns 500?

No. Check response.ok or status. A network failure is different from an HTTP response that indicates an application or server error.

Should every API request use a timeout?

A timeout is useful, but the duration should reflect the operation. A quick profile lookup and a long-running report may need different limits and cancellation behavior.

Editorial note: examples are provided for learning. Test them in your own environment and review security, privacy and compatibility requirements before using them in production.