Tutorials Logic, IN info@tutorialslogic.com

AJAX Error Handling Network HTTP Errors: Causes and Fixes

Failure Pipeline

A robust AJAX flow separates transport failure, HTTP failure, response-decoding failure, validation failure, cancellation, and stale results. The Fetch API rejects its promise for request failures such as an unavailable network, but a 404 or 500 still resolves to a Response. Application code must inspect response.ok or response.status before treating the body as success.

After this lesson, you can preserve useful server error details, give the user a recoverable state, cancel obsolete work, and avoid showing an older response after a newer request has completed.

Network Errors vs HTTP Errors

There are two distinct categories of errors in AJAX:

  • Network errors - the request never reached the server (no internet, DNS failure, server down, CORS block). With fetch(), the Promise rejects in this case.
  • HTTP errors - the server responded, but with an error status (400, 401, 403, 404, 500). With fetch(), the Promise resolves even for these - you must check response.ok manually.

Handling Both Error Types with Fetch

Handling Both Error Types with Fetch
async function fetchData(url) {
  try {
    const response = await fetch(url);

    // HTTP errors: fetch resolves but response.ok is false
    if (!response.ok) {
      // Try to read error details from the response body
      let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
      try {
        const errorBody = await response.json();
        errorMessage = errorBody.message || errorMessage;
      } catch {
        // response body wasn't JSON - use the status text
      }
      throw new Error(errorMessage);
    }

    return await response.json();

  } catch (error) {
    if (error.name === 'TypeError') {
      // Network error - fetch rejected (no internet, CORS, etc.)
      console.error('Network error:', error.message);
      showUserError('No internet connection. Please try again.');
    } else if (error.name === 'AbortError') {
      console.warn('Request was cancelled');
    } else {
      // HTTP error we threw above
      console.error('Request failed:', error.message);
      showUserError(error.message);
    }
    return null;
  }
}

function showUserError(message) {
  const el = document.getElementById('error-banner');
  el.textContent = message;
  el.style.display = 'block';
}

Timeout Handling with AbortController

Timeout Handling with AbortController
// Reusable fetch with timeout
async function fetchWithTimeout(url, options = {}, timeoutMs = 8000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(url, { ...options, signal: controller.signal });
    clearTimeout(timeoutId);

    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return await response.json();

  } catch (error) {
    clearTimeout(timeoutId);
    if (error.name === 'AbortError') {
      throw new Error(`Request timed out after ${timeoutMs}ms`);
    }
    throw error;
  }
}

// Usage
fetchWithTimeout('/api/data', {}, 5000)
  .then(data => console.log(data))
  .catch(err => console.error(err.message));

Retry Logic with Exponential Backoff

Retry Logic with Exponential Backoff
// Retry a fetch up to maxRetries times with exponential backoff
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  let lastError;

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

      // Don't retry client errors (4xx) - only server errors (5xx) or network issues
      if (response.status >= 400 && response.status < 500) {
        throw new Error(`Client error: ${response.status}`);
      }
      if (!response.ok) throw new Error(`Server error: ${response.status}`);

      return await response.json();

    } catch (error) {
      lastError = error;

      // Don't retry client errors
      if (error.message.startsWith('Client error')) throw error;

      if (attempt < maxRetries) {
        const delay = Math.pow(2, attempt) * 500; // 1s, 2s, 4s
        console.warn(`Attempt ${attempt} failed. Retrying in ${delay}ms...`);
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }

  throw new Error(`All ${maxRetries} attempts failed: ${lastError.message}`);
}

// Usage
fetchWithRetry('/api/unstable-endpoint')
  .then(data => console.log('Success:', data))
  .catch(err => console.error('Gave up:', err.message));

Response Classification

Check the status before decoding the success model. An error response may use a different JSON schema, plain text, or no body at all. Read the Content-Type header or use a tolerant error parser, retain a safe message and correlation ID, and throw an application error that the UI can classify without leaking server internals.

Treat 400-series statuses according to their meaning: validation feedback belongs near fields, 401 may begin reauthentication, 403 should explain insufficient access without retrying, 404 can show a missing-resource state, and 429 should honor Retry-After when supplied. A 500-series failure may be transient, but unlimited immediate retries can worsen an outage.

Cancellation and Races

AbortController lets code cancel a fetch when the user navigates away or a newer search supersedes the old one. Pass controller.signal to fetch and call abort on cleanup. Cancellation is an expected control-flow outcome, so do not display it as a scary network error.

Cancellation alone may not prevent every race if work has already completed. Track the current request identity or compare the query associated with the result before updating state. The visible result must correspond to the latest user intent, not whichever response happened to arrive last.

User Recovery

Keep the previous successful content visible when a background refresh fails, and label it as potentially stale when that matters. For an initial load with no usable data, show an error state with a focused retry action. Disable only the control whose request is pending rather than freezing the entire interface.

Log diagnostic context on the server and use a correlation ID in the client-facing message. Never display stack traces, SQL messages, tokens, or raw HTML returned by an upstream service. Error handling is part of the API contract and should be tested for representative statuses, malformed bodies, timeouts, and cancellation.

Before you move on

Failure Review

4 checks
  • Inspect HTTP status before decoding success data.
  • Handle absent or non-JSON error bodies safely.
  • Abort obsolete requests and prevent stale results from winning.
  • Give the user a retry path without exposing internals.

Hidden Failures

  • Assuming fetch rejects on 404 or 500.

    Check response.ok and classify the status explicitly.
  • Calling response.json for every error.

    Respect Content-Type and tolerate empty or text bodies.
  • Showing an aborted search as a network failure.

    Recognize cancellation as expected superseded work.

Try this next

Test the Failure Paths

0 of 2 completed

Next Step
Next Practice

Finish the concept here, then reinforce it with hands-on coding, interview prep, or a tool that matches the topic.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.