Fetch API Error Handling Patterns: Build Reliable JavaScript Requests
JavaScriptFetch APIAPIsError HandlingCode Snippets

Fetch API Error Handling Patterns: Build Reliable JavaScript Requests

JJS Tools Editorial Team
2026-11-06
7 min read

A practical checklist for fetch error handling, including HTTP checks, timeouts, retries, cancellation, invalid JSON, and UI fallbacks.

Reliable fetch() code does more than send a request and catch an exception. This guide provides a reusable checklist for handling HTTP errors, network failures, timeouts, retries, malformed JSON, cancellation, and user-facing fallbacks in browser-based JavaScript applications.

Overview

The Fetch API returns a promise that usually rejects for network-level failures, such as an unavailable connection or a request blocked by the browser. It does not automatically reject when a server responds with an HTTP error status such as 404 or 500. Your code must inspect response.ok or response.status and decide what those responses mean for the application.

A practical error-handling workflow has five stages:

  1. Validate the request inputs before sending.
  2. Apply a timeout or cancellation policy.
  3. Check the HTTP response before treating it as successful.
  4. Parse the response using a format appropriate to the endpoint.
  5. Return a useful result or display a controlled fallback.

Keeping these stages separate makes failures easier to diagnose. It also prevents a common problem: one broad try...catch block that hides whether the request failed because of connectivity, authorization, server behavior, invalid data, or user cancellation.

A small baseline pattern

async function getUser(userId) {
  const response = await fetch(`/api/users/${encodeURIComponent(userId)}`);

  if (!response.ok) {
    throw new Error(`Request failed with HTTP ${response.status}`);
  }

  return response.json();
}

try {
  const user = await getUser('42');
  console.log(user);
} catch (error) {
  console.error('Could not load user:', error);
}

This is a useful starting point, but production requests often need a timeout, clearer error categories, bounded retries, and protection against invalid response bodies.

Checklist by scenario

When the server returns an HTTP error

Check response.ok immediately after fetch() resolves. If you need different behavior for authentication, validation, rate limiting, or server errors, inspect the status explicitly.

if (response.status === 401) {
  throw new Error('Authentication is required');
}

if (response.status === 422) {
  throw new Error('The submitted data was not accepted');
}

if (response.status >= 500) {
  throw new Error('The service is temporarily unavailable');
}

if (!response.ok) {
  throw new Error(`Unexpected HTTP status: ${response.status}`);
}

If an API provides structured error details, read them carefully and use a safe fallback when the body is missing or has a different format. Do not assume that every error response is valid JSON.

When the request needs a timeout

A request can remain pending longer than the interface should wait. Use AbortController with a timer, and clear the timer after the request completes. The following helper distinguishes a timeout from other failures by attaching a reason.

async function fetchWithTimeout(url, options = {}, timeoutMs = 8000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => {
    controller.abort('timeout');
  }, timeoutMs);

  try {
    return await fetch(url, {
      ...options,
      signal: controller.signal
    });
  } catch (error) {
    if (controller.signal.aborted && controller.signal.reason === 'timeout') {
      throw new Error('The request timed out');
    }
    throw error;
  } finally {
    clearTimeout(timeoutId);
  }
}

Choose a timeout based on the interaction. A search suggestion request may need a shorter limit than a report download. A timeout should also be communicated consistently: users should know whether they can retry, continue without the data, or try again later.

When the user or application cancels a request

Cancellation is not always an error. For example, a search interface may abort an older request when the user types a new query. Reusing a caller-provided signal lets the application make that decision.

async function loadResults(query, signal) {
  const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
    signal
  });

  if (!response.ok) {
    throw new Error(`Search failed with HTTP ${response.status}`);
  }

  return response.json();
}

const controller = new AbortController();
loadResults('javascript tools', controller.signal)
  .catch(error => {
    if (error.name === 'AbortError') return;
    console.error(error);
  });

// Call this when the result is no longer needed:
// controller.abort();

Do not show a prominent failure message for an intentional cancellation unless the product requires it. Treat cancellation separately from a network outage or server error.

When retrying might help

Retries are appropriate only when repeating the request is safe and the failure may be temporary. They are more suitable for selected network failures or server-side responses than for malformed input, authentication failures, or most client-side validation errors. Be especially cautious with non-idempotent operations such as creating an order or submitting a payment.

function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function fetchWithRetry(url, options = {}, settings = {}) {
  const maxRetries = settings.maxRetries ?? 2;
  const baseDelay = settings.baseDelay ?? 300;

  for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
    try {
      const response = await fetch(url, options);

      const retryableStatus = response.status === 408 ||
        response.status === 429 ||
        response.status >= 500;

      if (response.ok) return response;
      if (!retryableStatus || attempt === maxRetries) return response;
    } catch (error) {
      if (attempt === maxRetries) throw error;
    }

    await wait(baseDelay * 2 ** attempt);
  }

  throw new Error('Retry limit reached');
}

Use a maximum attempt count and a delay that increases between attempts. If the server supplies a retry delay, your application may choose to respect it after validating the value. Never create an unbounded retry loop.

When the response body is invalid

response.json() can reject if the body is empty or malformed. It can also be the wrong parser when an endpoint returns text, a file, or another representation. Check the expected content type when the API contract requires it.

async function readJsonResponse(response) {
  const contentType = response.headers.get('content-type') || '';

  if (!contentType.includes('application/json')) {
    throw new Error('Expected a JSON response');
  }

  try {
    return await response.json();
  } catch {
    throw new Error('The server returned invalid JSON');
  }
}

What to double-check

  • Request method and body: Match the method, headers, and body format to the API contract. Serialize JSON explicitly and set the corresponding content type when needed.
  • Credentials and authentication: Confirm whether the request needs credentials, a bearer token, or a refreshed session. Avoid placing sensitive tokens in logs or user-visible error messages. For related token troubleshooting, see the JWT debugging guide.
  • Response assumptions: Decide whether an empty successful response is valid. Do not call response.json() automatically if an endpoint can return no content.
  • Duplicate submissions: Disable a submit action, use an idempotency strategy where supported, or cancel stale requests so a retry does not create unintended side effects.
  • Logging: Record enough context to diagnose failures, such as the operation and status code, while excluding credentials, personal data, and full request bodies where they are sensitive.
  • UI state: Model loading, success, empty, failure, and cancellation states explicitly. A failed request should not leave a spinner running indefinitely.

For payload-heavy integrations, a browser-based JSON formatter or validator can help inspect sample responses during development. The broader collection of online JSON tools for developers can be useful when comparing expected and actual API data.

Common mistakes

  1. Assuming fetch() rejects on 404 or 500: It generally resolves with a response, so status checks are still required.
  2. Catching and replacing every error with one message: A timeout, canceled request, invalid form, and authorization failure may require different actions.
  3. Retrying every request: Automatic retries can duplicate side effects and increase load. Limit retries to operations and failures where repetition is safe.
  4. Forgetting to clear timeout timers: Always clean up timers in a finally block or equivalent lifecycle cleanup.
  5. Parsing before checking the status: An error page or proxy response may not match the success schema. Check status first, then parse according to the response you received.
  6. Displaying raw server errors: Server messages can expose implementation details or confuse users. Map technical failures to safe, actionable interface text.
  7. Ignoring stale responses: In search, filtering, and route changes, an older request can finish after a newer one. Abort stale work or verify that the response still belongs to the current view.

When to revisit

Revisit this fetch error-handling checklist whenever an API contract changes, a request moves from a simple page load into a user action, or an integration begins showing intermittent failures. It is also worth reviewing before a seasonal planning cycle, a major release, or a change to authentication and session behavior.

As browser APIs, application frameworks, and backend conventions evolve, verify the cancellation and timeout behavior used by your project rather than copying a helper unchanged. Test at least these paths in development or automated tests:

  • Successful response with valid JSON.
  • Successful response with an empty body, if applicable.
  • Client error such as invalid input or unauthorized access.
  • Server error and a response that is not valid JSON.
  • Network failure or offline behavior.
  • Timeout and intentional cancellation.
  • Retry exhaustion and recovery after a temporary failure.
  • Two overlapping requests where the older response arrives last.

Before shipping, confirm that each failure has a bounded outcome: the promise settles, timers and listeners are cleaned up, sensitive details stay out of logs, and the user receives a clear next step. This simple review turns scattered fetch() calls into a consistent request layer that is easier to maintain and safer to reuse.

Related Topics

#JavaScript#Fetch API#APIs#Error Handling#Code Snippets
J

JS Tools Editorial Team

Technical Editor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.