Advanced JS Promise Patterns: Complete Tutorial

A complete tutorial on advanced JavaScript promise patterns. Covers Promise.allSettled for resilient parallel execution, Promise.race for timeouts, Promise.any for fastest success, sequential promise chains, promise pooling for concurrency control, retry with promises, promise memoization, and composing complex async workflows.

JavaScriptintermediate
16 min read

Beyond basic async/await, JavaScript promises enable powerful patterns for parallel execution, timeout management, concurrency control, and resilient error handling. This guide covers production-grade patterns that solve real-world async coordination problems.

Promise Combinators Overview

MethodResolves WhenRejects WhenUse Case
Promise.allAll promises fulfillAny promise rejectsParallel tasks where all must succeed
Promise.allSettledAll promises settleNever rejectsParallel tasks with partial failure tolerance
Promise.raceFirst promise settlesFirst promise rejectsTimeouts, fastest response
Promise.anyFirst promise fulfillsAll promises rejectFastest success from multiple sources

Promise.allSettled for Resilient Loading

javascriptjavascript
async function loadDashboard(userId) {
  const results = await Promise.allSettled([
    fetch(`/api/users/${userId}`).then((r) => r.json()),
    fetch(`/api/users/${userId}/projects`).then((r) => r.json()),
    fetch(`/api/notifications`).then((r) => r.json()),
    fetch(`/api/analytics/summary`).then((r) => r.json()),
  ]);
 
  const [userResult, projectsResult, notificationsResult, analyticsResult] = results;
 
  return {
    user: userResult.status === "fulfilled" ? userResult.value : null,
    projects: projectsResult.status === "fulfilled" ? projectsResult.value : [],
    notifications: notificationsResult.status === "fulfilled" ? notificationsResult.value : [],
    analytics: analyticsResult.status === "fulfilled" ? analyticsResult.value : null,
    errors: results
      .filter((r) => r.status === "rejected")
      .map((r) => r.reason.message),
  };
}

The dashboard loads even if analytics or notifications fail. Promise.all would have rejected the entire operation.

Promise.race for Timeouts

javascriptjavascript
function withTimeout(promise, ms, errorMessage = "Operation timed out") {
  const timeout = new Promise((_, reject) => {
    setTimeout(() => reject(new Error(errorMessage)), ms);
  });
 
  return Promise.race([promise, timeout]);
}
 
// Usage
async function getUser(id) {
  try {
    const user = await withTimeout(
      fetch(`/api/users/${id}`).then((r) => r.json()),
      5000,
      `User ${id} request timed out`
    );
    return user;
  } catch (error) {
    console.error(error.message);
    return null;
  }
}

Timeout With Cleanup

javascriptjavascript
function withTimeoutAndAbort(fetchFn, ms) {
  const controller = new AbortController();
 
  const timeout = new Promise((_, reject) => {
    setTimeout(() => {
      controller.abort();
      reject(new Error(`Request timed out after ${ms}ms`));
    }, ms);
  });
 
  return Promise.race([fetchFn(controller.signal), timeout]);
}
 
// Usage
const data = await withTimeoutAndAbort(
  (signal) => fetch("/api/heavy-data", { signal }).then((r) => r.json()),
  10000
);

See using AbortController in JS complete tutorial for comprehensive cancellation patterns.

Promise.any for Fastest Success

javascriptjavascript
async function fetchFromMirrors(resource) {
  const mirrors = [
    `https://cdn1.example.com/${resource}`,
    `https://cdn2.example.com/${resource}`,
    `https://cdn3.example.com/${resource}`,
  ];
 
  try {
    const response = await Promise.any(
      mirrors.map((url) => fetch(url).then((r) => {
        if (!r.ok) throw new Error(`${url} returned ${r.status}`);
        return r;
      }))
    );
 
    return response.json();
  } catch (error) {
    // AggregateError: all mirrors failed
    console.error("All mirrors failed:", error.errors.map((e) => e.message));
    throw error;
  }
}

Promise.any resolves as soon as the first promise fulfills. If all reject, it throws an AggregateError containing all rejection reasons.

Sequential Promise Chain

javascriptjavascript
async function processSequentially(items, asyncFn) {
  const results = [];
 
  for (const item of items) {
    const result = await asyncFn(item);
    results.push(result);
  }
 
  return results;
}
 
// Usage: process one at a time (order matters)
const users = await processSequentially(
  [1, 2, 3, 4, 5],
  async (id) => {
    const res = await fetch(`/api/users/${id}`);
    return res.json();
  }
);

Sequential With Reduce

javascriptjavascript
async function processWithReduce(items, asyncFn) {
  return items.reduce(async (prevPromise, item) => {
    const results = await prevPromise;
    const result = await asyncFn(item);
    return [...results, result];
  }, Promise.resolve([]));
}

Concurrency Pool

Run N promises at a time from a larger set:

javascriptjavascript
async function pool(tasks, concurrency) {
  const results = [];
  const executing = new Set();
 
  for (const [index, task] of tasks.entries()) {
    const promise = task().then((result) => {
      executing.delete(promise);
      results[index] = { status: "fulfilled", value: result };
    }).catch((error) => {
      executing.delete(promise);
      results[index] = { status: "rejected", reason: error };
    });
 
    executing.add(promise);
 
    if (executing.size >= concurrency) {
      await Promise.race(executing);
    }
  }
 
  await Promise.all(executing);
  return results;
}
 
// Usage: fetch 100 URLs, 5 at a time
const urls = Array.from({ length: 100 }, (_, i) => `https://api.example.com/items/${i}`);
const tasks = urls.map((url) => () => fetch(url).then((r) => r.json()));
 
const results = await pool(tasks, 5);

Pool Size Guidelines

Pool SizeUse CaseTrade-off
1Sequential processingSlowest, least server load
3-5API calls with rate limitsGood balance
10-20Internal microservicesFast, moderate load
50+Local file operationsMaximum throughput

Retry With Promises

javascriptjavascript
async function retry(fn, options = {}) {
  const maxRetries = options.maxRetries || 3;
  const baseDelay = options.baseDelay || 1000;
  const shouldRetry = options.shouldRetry || (() => true);
 
  let lastError;
 
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn(attempt);
    } catch (error) {
      lastError = error;
 
      if (attempt === maxRetries || !shouldRetry(error, attempt)) {
        throw error;
      }
 
      const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 500;
      console.warn(`Attempt ${attempt + 1} failed. Retrying in ${Math.round(delay)}ms`);
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
 
  throw lastError;
}
 
// Usage
const data = await retry(
  () => fetch("/api/data").then((r) => {
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
    return r.json();
  }),
  {
    maxRetries: 3,
    baseDelay: 1000,
    shouldRetry: (error) => !error.message.includes("HTTP 4"),
  }
);

See API retry patterns in JavaScript full tutorial for advanced retry strategies.

Promise Memoization

javascriptjavascript
function memoizeAsync(fn, options = {}) {
  const cache = new Map();
  const ttl = options.ttl || 60000; // 1 minute default
 
  return async function (...args) {
    const key = options.keyFn ? options.keyFn(...args) : JSON.stringify(args);
 
    const cached = cache.get(key);
    if (cached && Date.now() - cached.timestamp < ttl) {
      return cached.value;
    }
 
    // Store the promise itself to prevent duplicate in-flight requests
    const promise = fn.apply(this, args);
    cache.set(key, { value: promise, timestamp: Date.now() });
 
    try {
      const result = await promise;
      cache.set(key, { value: Promise.resolve(result), timestamp: Date.now() });
      return result;
    } catch (error) {
      cache.delete(key); // Remove failed entries
      throw error;
    }
  };
}
 
// Usage
const getUser = memoizeAsync(
  async (id) => {
    const res = await fetch(`/api/users/${id}`);
    return res.json();
  },
  { ttl: 30000 }
);
 
// First call fetches, second call returns cached
const user1 = await getUser(42);
const user2 = await getUser(42); // instant, no network

Composing Async Workflows

javascriptjavascript
function pipe(...fns) {
  return async (input) => {
    let result = input;
    for (const fn of fns) {
      result = await fn(result);
    }
    return result;
  };
}
 
// Define steps
const fetchUser = async (userId) => {
  const res = await fetch(`/api/users/${userId}`);
  return res.json();
};
 
const enrichWithProjects = async (user) => {
  const res = await fetch(`/api/users/${user.id}/projects`);
  const projects = await res.json();
  return { ...user, projects };
};
 
const calculateStats = async (user) => {
  return {
    ...user,
    stats: {
      totalProjects: user.projects.length,
      activeProjects: user.projects.filter((p) => p.status === "active").length,
    },
  };
};
 
// Compose the pipeline
const getUserWithStats = pipe(fetchUser, enrichWithProjects, calculateStats);
 
const result = await getUserWithStats(42);
Rune AI

Rune AI

Key Insights

  • Promise.allSettled never rejects: It waits for every promise to settle and returns statuses, making it ideal for independent parallel operations
  • Promise.race does not cancel losers: Combine with AbortController to actually stop losing requests and free resources
  • Concurrency pools prevent server overload: Run N operations at a time from a larger set using a Set of executing promises and Promise.race
  • Memoize the promise, not just the result: Storing the pending promise deduplicates in-flight requests from concurrent callers
  • Retry only transient errors: Network failures, 429, and 5xx responses are retryable; 4xx responses are permanent client errors
RunePowered by Rune AI

Frequently Asked Questions

When should I use Promise.all vs Promise.allSettled?

Use `Promise.all` when all promises must succeed (loading related data for a single view). Use `Promise.allSettled` when partial failure is acceptable (dashboard widgets that can show fallback states independently).

How does Promise.race handle the losing promises?

The losing promises continue executing in the background. `Promise.race` does not cancel them. Use `AbortController` to actually cancel losing fetch requests to avoid wasted bandwidth.

What is the best concurrency pool size for API calls?

3-5 for public APIs (they usually rate limit). 10-20 for internal services. Always respect the API's `X-RateLimit-Remaining` header. See [rate limiting in JavaScript complete tutorial](/tutorials/programming-languages/javascript/rate-limiting-in-javascript-complete-tutorial) for client-side rate limiting.

Should I retry on all errors?

No. Only retry on transient errors (network failures, 5xx, 429). Never retry 4xx errors (except 429) since they indicate a problem with the request itself. See [advanced API error handling in JS full guide](/tutorials/programming-languages/javascript/advanced-api-error-handling-in-js-full-guide) for error categorization.

How do I prevent duplicate in-flight requests?

Memoize the promise itself, not just the result. When a second call arrives while the first is pending, return the same promise. This is shown in the memoization pattern above.

Conclusion

Advanced promise patterns solve real coordination problems: Promise.allSettled for partial-failure tolerance, Promise.race for timeouts, concurrency pools for controlled parallelism, retry for transient failures, and memoization for deduplication. The key insight is that promises are values you can store, race, combine, and chain. For cancellation with AbortController, see using AbortController in JS complete tutorial. For the event loop that schedules promise microtasks, see the JS event loop architecture complete guide.