Advanced Promise Patterns — Concurrency Limits, Retries, Timeouts

Promise Method Recap

Before diving into practical patterns, you need a solid understanding of the differences between Promise.all, Promise.allSettled, Promise.race, and Promise.any. Each method combines multiple Promises differently.

// Helper to create succeeding/failing Promises
const success = (value, ms) =>
  new Promise((resolve) => setTimeout(() => resolve(value), ms));
const fail = (reason, ms) =>
  new Promise((_, reject) => setTimeout(() => reject(reason), ms));

// Promise.all — all must succeed (rejects immediately if any fails)
async function allExample() {
  try {
    const results = await Promise.all([
      success("A", 100),
      success("B", 200),
      fail("C failed", 150),  // This causes the entire call to fail
    ]);
  } catch (error) {
    console.log("all failed:", error);  // all failed: C failed
  }
}

// Promise.allSettled — waits until all complete (continues on failure)
async function allSettledExample() {
  const results = await Promise.allSettled([
    success("A", 100),
    fail("B failed", 200),
    success("C", 150),
  ]);

  results.forEach((result) => {
    if (result.status === "fulfilled") {
      console.log("Success:", result.value);
    } else {
      console.log("Failed:", result.reason);
    }
  });
  // Success: A
  // Failed: B failed
  // Success: C
}

// Promise.race — returns the first to complete
// Promise.any — returns the first to succeed
MethodOn single failureOn all failureReturns
allRejects immediatelyRejectsResult array
allSettledContinuesReturns all resultsStatus+value array
raceRejects if failure comes firstRejectsFirst result
anyContinuesAggregateErrorFirst success

Concurrency Limiting — Promise Pool

Running hundreds of async tasks simultaneously can overload the server. A Pool pattern that limits concurrent execution is needed.

async function promisePool(tasks, concurrency = 5) {
  /**
   * Executes Promises with a concurrency limit.
   * @param {Function[]} tasks - Array of functions in () => Promise form
   * @param {number} concurrency - Maximum number of concurrent tasks
   * @returns {Promise<any[]>} Result array (preserves input order)
   */
  const results = new Array(tasks.length);
  let currentIndex = 0;

  async function worker() {
    while (currentIndex < tasks.length) {
      const index = currentIndex++;
      try {
        results[index] = await tasks[index]();
      } catch (error) {
        results[index] = { error };
      }
    }
  }

  // Create workers equal to the concurrency limit
  const workers = Array.from(
    { length: Math.min(concurrency, tasks.length) },
    () => worker()
  );
  await Promise.all(workers);
  return results;
}

// Usage: execute 100 API requests, 5 at a time
async function fetchAllUsers() {
  const userIds = Array.from({ length: 100 }, (_, i) => i + 1);
  const tasks = userIds.map(
    (id) => () => fetchUser(id) // () => Promise form
  );

  console.time("100 requests");
  const results = await promisePool(tasks, 5);
  console.timeEnd("100 requests");
  // 100 requests: 2034ms (5 concurrent, assuming 100ms per request)

  const succeeded = results.filter((r) => !r?.error).length;
  console.log(`Succeeded: ${succeeded}, Failed: ${100 - succeeded}`);
}

// Simulation fetch function
async function fetchUser(id) {
  await new Promise((r) => setTimeout(r, 100));
  if (id % 17 === 0) throw new Error(`User ${id} not found`);
  return { id, name: `User_${id}` };
}

Retry Pattern — Exponential Backoff

A pattern for retrying with exponential backoff when network requests fail temporarily.

async function withRetry(fn, options = {}) {
  /**
   * Retries with exponential backoff on failure.
   * @param {Function} fn - Async function to execute
   * @param {object} options - Retry configuration
   */
  const {
    maxRetries = 3,        // Maximum retry count
    baseDelay = 1000,      // Base delay (ms)
    maxDelay = 30000,      // Maximum delay (ms)
    backoffFactor = 2,     // Backoff multiplier
    shouldRetry = () => true,  // Function to determine whether to retry
  } = options;

  let lastError;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn(attempt);
    } catch (error) {
      lastError = error;

      if (attempt === maxRetries || !shouldRetry(error)) {
        break;
      }

      // Exponential backoff + jitter
      const delay = Math.min(
        baseDelay * Math.pow(backoffFactor, attempt),
        maxDelay
      );
      const jitter = delay * 0.2 * Math.random(); // 20% jitter
      const waitTime = delay + jitter;

      console.log(
        `[Retry] ${attempt + 1}/${maxRetries} ` +
        `(after ${waitTime.toFixed(0)}ms): ${error.message}`
      );
      await new Promise((r) => setTimeout(r, waitTime));
    }
  }

  throw lastError;
}

// Usage example
async function fetchDataWithRetry() {
  const data = await withRetry(
    async (attempt) => {
      console.log(`Request attempt ${attempt + 1}`);
      const response = await fetch("https://api.example.com/data");
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      return response.json();
    },
    {
      maxRetries: 3,
      baseDelay: 1000,
      // Do not retry on 4xx errors (client errors)
      shouldRetry: (error) => !error.message.startsWith("HTTP 4"),
    }
  );
  return data;
}

Timeout Pattern

A pattern that throws an error if a task does not complete within a specified time.

function withTimeout(promise, ms, message) {
  /**
   * Applies a timeout to a Promise.
   * @param {Promise} promise - The Promise to apply a timeout to
   * @param {number} ms - Timeout duration (milliseconds)
   * @param {string} message - Timeout error message
   */
  const timeout = new Promise((_, reject) => {
    const timer = setTimeout(() => {
      reject(new Error(message || `Timeout exceeded: ${ms}ms`));
    }, ms);

    // Clean up timer if the Promise completes first
    promise.finally(() => clearTimeout(timer));
  });

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

// Usage example
async function fetchWithTimeout() {
  try {
    const data = await withTimeout(
      fetch("https://api.example.com/slow-endpoint"),
      5000,
      "API response timed out after 5 seconds"
    );
    return data.json();
  } catch (error) {
    console.error("Request failed:", error.message);
    // API response timed out after 5 seconds
  }
}

Composite Pattern — Retry + Timeout + Concurrency Limit

In practice, multiple patterns are combined together.

async function robustFetch(urls, options = {}) {
  /**
   * Fetches multiple URLs reliably.
   * Applies retry, timeout, and concurrency limiting.
   */
  const {
    concurrency = 3,
    timeout = 5000,
    maxRetries = 2,
  } = options;

  const tasks = urls.map(
    (url) => async () => {
      return withRetry(
        async () => {
          const response = await withTimeout(
            fetch(url),
            timeout,
            `${url} timed out`
          );
          if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${url}`);
          }
          return { url, data: await response.json() };
        },
        { maxRetries }
      );
    }
  );

  const results = await promisePool(tasks, concurrency);
  const succeeded = results.filter((r) => !r?.error);
  const failed = results.filter((r) => r?.error);

  console.log(`Done: ${succeeded.length} succeeded, ${failed.length} failed`);
  return { succeeded, failed };
}

// Usage example
// const urls = Array.from({ length: 50 }, (_, i) =>
//   `https://api.example.com/items/${i}`
// );
// const { succeeded, failed } = await robustFetch(urls, {
//   concurrency: 5,
//   timeout: 3000,
//   maxRetries: 2,
// });

Practical Tips

  • Prefer allSettled: For multiple independent requests, use allSettled instead of all. A single failure should not break everything.
  • Exponential backoff: Increase retry intervals exponentially and add jitter to prevent server overload from simultaneous retries.
  • Cancellable tasks: Use AbortController to cancel fetch requests.
  • Watch memory usage: Creating thousands of Promises simultaneously can spike memory. Always limit concurrency.
  • Classify errors: Distinguish between retryable errors (5xx, network) and non-retryable errors (4xx, auth).
  • Logging: Log retries and timeouts so issues can be traced.

Was this article helpful?