← Back to Home

How to Handle Timeouts in Async Operations

Updated January 14, 2026
timeoutpromiseasyncraceabort-controller

Handling Timeouts in Async Operations

Timeout handling prevents operations from hanging indefinitely.

Promise.race Pattern

function withTimeout<T>(
  promise: Promise<T>,
  timeoutMs: number,
  errorMessage: string = 'Operation timed out'
): Promise<T> {
  const timeoutPromise = new Promise<never>((_, reject) => {
    setTimeout(() => reject(new Error(errorMessage)), timeoutMs);
  });

  return Promise.race([promise, timeoutPromise]);
}

Usage

try {
  const result = await withTimeout(
    fetchData(),
    5000,
    'Fetch request timed out after 5 seconds'
  );
} catch (error) {
  if (error.message === 'Fetch request timed out after 5 seconds') {
    console.log('Request timed out');
  } else {
    console.log('Other error:', error);
  }
}

AbortController Pattern

For fetch requests and other abortable operations:

async function fetchWithTimeout(
  url: string,
  options: RequestInit = {},
  timeoutMs: number = 30000
): Promise<Response> {
  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) {
    clearTimeout(timeoutId);
    if (error.name === 'AbortError') {
      throw new Error(`Request timed out after ${timeoutMs}ms`);
    }
    throw error;
  }
}

Custom Timeout Error

class TimeoutError extends Error {
  constructor(message: string, public readonly timeoutMs: number) {
    super(message);
    this.name = 'TimeoutError';
  }
}

function withTimeout<T>(
  promise: Promise<T>,
  timeoutMs: number
): Promise<T> {
  return Promise.race([
    promise,
    new Promise<never>((_, reject) =>
      setTimeout(() => reject(new TimeoutError(
        `Operation timed out after ${timeoutMs}ms`,
        timeoutMs
      )), timeoutMs)
    ),
  ]);
}

Timeout with Cleanup

async function withTimeoutAndCleanup<T>(
  operation: () => Promise<T>,
  timeoutMs: number,
  cleanup: () => void | Promise<void>
): Promise<T> {
  let timeoutId: NodeJS.Timeout;

  const timeoutPromise = new Promise<never>((_, reject) => {
    timeoutId = setTimeout(() => {
      cleanup();  // Run cleanup on timeout
      reject(new Error(`Operation timed out after ${timeoutMs}ms`));
    }, timeoutMs);
  });

  try {
    return await Promise.race([operation(), timeoutPromise]);
  } finally {
    clearTimeout(timeoutId);
  }
}

Configurable Timeout Manager

class TimeoutManager {
  private activeTimeouts: Map<string, NodeJS.Timeout> = new Map();

  set(key: string, callback: () => void, delayMs: number): void {
    this.clear(key);  // Clear existing timeout if any
    const timeoutId = setTimeout(() => {
      callback();
      this.activeTimeouts.delete(key);
    }, delayMs);
    this.activeTimeouts.set(key, timeoutId);
  }

  clear(key: string): void {
    const timeoutId = this.activeTimeouts.get(key);
    if (timeoutId) {
      clearTimeout(timeoutId);
      this.activeTimeouts.delete(key);
    }
  }

  clearAll(): void {
    this.activeTimeouts.forEach((timeoutId) => clearTimeout(timeoutId));
    this.activeTimeouts.clear();
  }
}

Progressive Timeout

async function withProgressiveTimeout<T>(
  operation: () => Promise<T>,
  timeouts: number[]  // [1000, 2000, 5000, 10000]
): Promise<T> {
  for (let i = 0; i < timeouts.length; i++) {
    try {
      return await withTimeout(operation(), timeouts[i]);
    } catch (error) {
      if (i === timeouts.length - 1) {
        throw error;  // Re-throw on last attempt
      }
      console.log(`Timeout ${i + 1}/${timeouts.length} (${timeouts[i]}ms), retrying...`);
    }
  }
  throw new Error('All timeouts failed');
}

Timeout with Fallback

async function withTimeoutAndFallback<T>(
  operation: () => Promise<T>,
  timeoutMs: number,
  fallback: () => T
): Promise<T> {
  try {
    return await withTimeout(operation(), timeoutMs);
  } catch (error) {
    if (error.message.includes('timed out')) {
      console.log('Operation timed out, using fallback');
      return fallback();
    }
    throw error;
  }
}

For API Clients

class ApiClient {
  constructor(
    private baseUrl: string,
    private defaultTimeoutMs: number = 30000,
    private maxRetries: number = 3
  ) {}

  async get<T>(
    endpoint: string,
    timeoutMs?: number
  ): Promise<T> {
    const timeout = timeoutMs ?? this.defaultTimeoutMs;

    for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
      try {
        return await withTimeout(
          fetch(`${this.baseUrl}${endpoint}`).then(r => r.json()),
          timeout
        );
      } catch (error) {
        if (attempt === this.maxRetries || !error.message.includes('timed out')) {
          throw error;
        }
        // Retry with exponential backoff
        await sleep(Math.pow(2, attempt) * 1000);
      }
    }
    throw new Error('All attempts failed');
  }
}

Common Timeout Values

Operation Recommended Timeout
HTTP request 30 seconds
Database query 10 seconds
File read 5 seconds
AI generation 120 seconds
Image processing 30 seconds
Video render 600 seconds

Best Practices

  1. Always set timeouts for external operations
  2. Use descriptive error messages including timeout duration
  3. Clean up resources on timeout (connections, file handles)
  4. Log timeout events for monitoring
  5. Configure timeouts based on operation type
  6. Consider progressive timeouts for retries
  7. Use AbortController for fetch requests