API Timeout Handling | Blue Frog Docs

API Timeout Handling

Implement robust timeout handling for API requests

API Timeout Handling

What This Means

Without proper timeout handling, API requests can hang indefinitely, causing:

  • Frozen UI elements
  • Memory leaks
  • Poor user experience
  • Wasted resources on failed requests

How to Diagnose

Identify Hanging Requests

DevTools > Network:

  1. Look for requests with "Pending" status for long periods
  2. Check for requests without response
  3. Filter by time to find slow requests

Console Monitoring

// Log slow requests
const originalFetch = window.fetch;
window.fetch = async function(...args) {
  const start = performance.now();
  try {
    const response = await originalFetch.apply(this, args);
    const duration = performance.now() - start;
    if (duration > 3000) {
      console.warn(`Slow request (${duration}ms):`, args[0]);
    }
    return response;
  } catch (error) {
    console.error('Request failed:', args[0], error);
    throw error;
  }
};

General Fixes

Fetch with AbortController

// Basic timeout implementation
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

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

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

// Usage
try {
  const response = await fetchWithTimeout('/api/data', {}, 5000);
  const data = await response.json();
} catch (error) {
  if (error.message.includes('timed out')) {
    showTimeoutMessage();
  }
}

Configurable Timeout Utility

class ApiClient {
  constructor(baseUrl, defaultTimeout = 10000) {
    this.baseUrl = baseUrl;
    this.defaultTimeout = defaultTimeout;
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseUrl}${endpoint}`;
    const timeout = options.timeout || this.defaultTimeout;
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    const config = {
      ...options,
      signal: controller.signal,
      headers: {
        'Content-Type': 'application/json',
        ...options.headers
      }
    };

    try {
      const response = await fetch(url, config);
      clearTimeout(timeoutId);

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

      return await response.json();
    } catch (error) {
      clearTimeout(timeoutId);

      if (error.name === 'AbortError') {
        throw new TimeoutError(url, timeout);
      }
      throw error;
    }
  }

  get(endpoint, options) {
    return this.request(endpoint, { ...options, method: 'GET' });
  }

  post(endpoint, data, options) {
    return this.request(endpoint, {
      ...options,
      method: 'POST',
      body: JSON.stringify(data)
    });
  }
}

class TimeoutError extends Error {
  constructor(url, timeout) {
    super(`Request to ${url} timed out after ${timeout}ms`);
    this.name = 'TimeoutError';
    this.url = url;
    this.timeout = timeout;
  }
}

// Usage
const api = new ApiClient('https://api.example.com', 5000);
const data = await api.get('/users', { timeout: 3000 });

Axios Timeout

import axios from 'axios';

const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000, // 5 seconds
  timeoutErrorMessage: 'Request timed out'
});

// Per-request timeout
const response = await api.get('/users', { timeout: 3000 });

// Handle timeout errors
api.interceptors.response.use(
  response => response,
  error => {
    if (error.code === 'ECONNABORTED') {
      // Timeout error
      return Promise.reject(new Error('Request timed out'));
    }
    return Promise.reject(error);
  }
);

React Query with Timeout

import { useQuery } from '@tanstack/react-query';

async function fetchWithTimeout(url) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 5000);

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

function UserData() {
  const { data, error, isLoading } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetchWithTimeout('/api/users'),
    retry: (failureCount, error) => {
      // Don't retry on timeout
      if (error.name === 'AbortError') return false;
      return failureCount < 3;
    }
  });

  if (isLoading) return <Loading />;
  if (error) return <Error message={error.message} />;
  return <UserList users={data} />;
}

Progressive Timeout Strategy

// Increase timeout for retries
async function fetchWithProgressiveTimeout(url, options = {}) {
  const timeouts = [2000, 5000, 10000]; // Progressive timeouts
  let lastError;

  for (let i = 0; i < timeouts.length; i++) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeouts[i]);

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

      if (error.name === 'AbortError') {
        console.log(`Attempt ${i + 1} timed out after ${timeouts[i]}ms`);
        continue; // Try next timeout
      }
      throw error; // Non-timeout error, don't retry
    }
  }

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

UI Feedback for Timeouts

function LoadingWithTimeout({ timeout = 5000, onTimeout }) {
  const [showSlowMessage, setShowSlowMessage] = useState(false);

  useEffect(() => {
    const timer = setTimeout(() => {
      setShowSlowMessage(true);
    }, timeout / 2);

    const timeoutTimer = setTimeout(() => {
      onTimeout?.();
    }, timeout);

    return () => {
      clearTimeout(timer);
      clearTimeout(timeoutTimer);
    };
  }, [timeout, onTimeout]);

  return (
    <div className="loading">
      <Spinner />
      {showSlowMessage && (
        <p>This is taking longer than usual...</p>
      )}
    </div>
  );
}

Verification

  1. Test with Network throttling in DevTools
  2. Verify timeout errors are caught
  3. Check UI shows appropriate feedback
  4. Confirm no hung requests in Network tab

Further Reading

// SYS.FOOTER