Handling Real-World Network Conditions

In the real world, networks are unreliable. Good code must gracefully handle temporary failures.

1. Request Timeout

Don't wait forever if the server is silent:

async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
  
  try {
    const response = await fetch(url, { ...options, signal: controller.signal });
    clearTimeout(timeoutId);
    return response;
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error('Request timed out after ' + timeoutMs + 'ms');
    }
    throw error;
  }
}

2. Retry Logic with Exponential Backoff

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  let delay = 1000; // Start with 1 second
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);
      if (response.ok) return response;
      throw new Error('HTTP ' + response.status);
    } catch (error) {
      if (attempt === maxRetries) throw error;
      
      console.log(`Attempt ${attempt} failed. Retrying in ${delay}ms...`);
      await new Promise(resolve => setTimeout(resolve, delay));
      delay *= 2; // Exponential backoff: 1s → 2s → 4s
    }
  }
}
Important

Only retry safe requests (GET). Never auto-retry POST/DELETE with side effects — the first request may have succeeded before the network error occurred, and retrying could create duplicates (e.g., double payments!).

Tip

Exponential backoff (waiting 1s, then 2s, then 4s...) gives the server time to recover instead of hammering it with immediate retries.