DEV Community

omri luz
omri luz

Posted on

9

Promise Combinators: race, allSettled, any

Promise Combinators: Promise.race, Promise.allSettled, and Promise.any

Introduction

Promises in JavaScript represent a powerful asynchronous programming pattern that allows developers to manage the complexity of operations that take place over time. With the evolution of JavaScript, particularly with the introduction of ES6 (ECMAScript 2015), Promises have become a cornerstone for handling asynchronous tasks. However, as applications grow in complexity, the necessity of managing multiple promises concurrently arises. This is where promise combinators such as Promise.race, Promise.allSettled, and Promise.any come into play.

Understanding these combinators demands thorough insights into their historical context, underlying mechanics, use cases, edge cases, optimization strategies, and potential pitfalls. This guide aims to offer an exhaustive examination of these combinators, providing senior developers with the critical knowledge required to utilize them effectively.

Historical Context

The inception of Promises in JavaScript can be traced back to discussions in the TC39 committee, which aims for the evolution of ECMAScript. Prior to their widespread adoption, developers often relied on callback functions, leading to callback hell — a state characterized by deeply nested functions that dramatically impede readability and maintainability of code.

The introduction of Promises alleviated many of these concerns by providing a cleaner, more manageable way to perform asynchronous operations. However, real-world applications often require coordinating the outcomes of multiple asynchronous tasks, leading to the need for promise combinators.

ECMAScript 2015 (ES6) and Beyond

With the rollout of ES6, the standardization of the Promise object included several built-in methods:

  1. Promise.all - Waits for all promises to resolve and returns an array of resolved values; if one promise rejects, it rejects with that reason.
  2. Promise.race - Returns a promise that resolves or rejects as soon as one of the promises in the iterable resolves or rejects.
  3. Promise.allSettled - Allows handling of multiple promises and returns a promise that resolves after all promises have settled, regardless of their outcome.
  4. Promise.any - Similar to Promise.all, but resolves as soon as one of the promises rejects; if all promises are rejected, it ultimately rejects.

These combinators significantly enhance the control flow in asynchronous JavaScript, particularly in complex application scenarios.

In-Depth Exploration of Promise Combinators

1. Promise.race

Description:

Promise.race takes an iterable of promises and returns a new promise that resolves or rejects as soon as one of the promises in the iterable settles (either resolves or rejects).

Usage:

const promise1 = new Promise((resolve, reject) => setTimeout(resolve, 200, 'First'));
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'Second'));

Promise.race([promise1, promise2])
  .then(result => {
    console.log(result); // "First" or "Second" depending on which finishes first
  })
  .catch(error => {
    console.error(error); // "Second" if it rejects first
  });
Enter fullscreen mode Exit fullscreen mode

Edge Cases & Advanced Examples:

When using Promise.race, the order of promise execution may lead to unexpected behaviors, especially when one promise has a much longer execution time than others.

Consider the example of a race between a database lookup and a timeout:

const dbLookup = new Promise((resolve) => setTimeout(resolve, 500, 'Data from DB'));
const timeout = new Promise((_, reject) => setTimeout(reject, 300, 'Request timed out'));

Promise.race([dbLookup, timeout])
  .then(console.log)
  .catch(console.error); // Will log "Request timed out"
Enter fullscreen mode Exit fullscreen mode

Performance Considerations

  • Short-circuited Resolution: If a promise resolves very quickly, Promise.race is optimal for managing time-sensitive operations, but beware of potentially blocking promises.
  • Resource Management: Long-running promises can consume resources unnecessarily that can be freed after early resolver behaviors. Consider terminating pending operations with an external mechanism if necessary.

2. Promise.allSettled

Description:

Introduced in ECMAScript 2020, Promise.allSettled allows the collection of results from multiple promises, regardless of whether they resolve or reject.

Usage:

const p1 = Promise.resolve(3);
const p2 = Promise.reject('error');
const p3 = Promise.resolve(42);

Promise.allSettled([p1, p2, p3])
  .then(results => results.forEach((result) => console.log(result)));
Enter fullscreen mode Exit fullscreen mode

Output:

{ status: "fulfilled", value: 3 }
{ status: "rejected", reason: "error" }
{ status: "fulfilled", value: 42 }
Enter fullscreen mode Exit fullscreen mode

Complex Scenarios:

This combinator is particularly useful when you're aggregating results from multiple API calls, allowing for graceful degradation in cases of failure:

const apiCalls = [
  fetch('/api/endpoint1'),
  fetch('/api/endpoint2'),
  fetch('/api/endpoint3')
];

Promise.allSettled(apiCalls)
  .then(results => {
    results.forEach((result, index) => {
      if (result.status === 'fulfilled') {
        console.log(`API ${index + 1} succeeded with data:`, result.value);
      } else {
        console.error(`API ${index + 1} failed:`, result.reason);
      }
    });
  });
Enter fullscreen mode Exit fullscreen mode

3. Promise.any

Description:

Promise.any resolves as soon as one of the promises in the iterable resolves, or rejects if no promises in the iterable fulfill (i.e., all reject).

Usage:

const p1 = Promise.reject('One');
const p2 = Promise.reject('Two');
const p3 = Promise.resolve('Three');

Promise.any([p1, p2, p3])
  .then(result => console.log(result)) // "Three"
  .catch(error => console.error(error)); // If all reject
Enter fullscreen mode Exit fullscreen mode

Real-World Application:

In scenarios like API fallbacks, where multiple endpoints are available but only one needs to succeed, Promise.any is beneficial.

function fetchWithFallback() {
  return Promise.any([
    fetch('https://api.primary.com/data'),
    fetch('https://api.secondary.com/data'),
    fetch('https://api.tertiary.com/data')
  ]);
}

fetchWithFallback()
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('All endpoints failed:', error));
Enter fullscreen mode Exit fullscreen mode

Comparison with Alternative Approaches

Asynchronous Iteration vs. Promise Combinators

JavaScript's for..of loops combined with async/await allow for sequential processing of promises, which can have their place. However, promise combinators significantly optimally manage concurrency. The interactive nature of combinators allows for performance improvements in scenarios requiring parallel execution.

For single tasks, await may often be simpler:

async function processSequentially() {
  const response1 = await fetch('/api/data1');
  const response2 = await fetch('/api/data2');
  return [response1, response2];
}
Enter fullscreen mode Exit fullscreen mode

In contrast, combinators willingly offload to the event loop for more scalable operations across multiple tasks.

Performance Considerations and Optimization Strategies

  • Batching Requests: For applications that deal with network requests, consider batching multiple requests to minimize network overhead while utilizing Promise.allSettled to assess the success of each request.
  • Cancellation Considerations: In environments like browsers, where resources must be managed (e.g., aborting fetch requests), ensure promises can handle cancellations to avoid memory leaks.

Debugging Techniques and Potential Pitfalls

  1. Unhandled Rejections: Utilize process handlers for unhandled promise rejections to log or manage errors effectively. In Node.js, enabling the warning for unhandled promise rejections can aid in debugging.

Example:

   process.on('unhandledRejection', (reason, promise) => {
       console.error('Unhandled Rejection at:', promise, 'reason:', reason);
   });
Enter fullscreen mode Exit fullscreen mode
  1. Tracking Promises: Use debugging tools or library solutions to track the resolution states of promises in complex applications to gain insights into asynchronous flows.

  2. Fallback Handling: Ensure to have reasonable fallback mechanisms on error handling when the operations performed by Promise.any can return critical failures.

Conclusion

This comprehensive guide provides an in-depth analysis of promise combinators — Promise.race, Promise.allSettled, and Promise.any. Armed with this knowledge, seasoned developers can leverage these tools to build more resilient and efficient asynchronous code.

For further reading and in-depth exploration, we recommend checking out the following resources and official documentation:

By understanding these advanced concepts and implementing them judiciously, developers can navigate the complexities of modern web applications with greater ease.

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (1)

Collapse
 
nidal_tahir_cde5660ddbe04 profile image
Nidal tahir

Great breakdown of Promise combinators! One thing most people overlook is that Promise.race can actually be performance gold for real-world applications - pair it with AbortController and you've got a clean way to prevent those zombie requests that silently eat up connection pools. Still amazed how many devs default to Promise.all without considering the more resilient alternatives!

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

👋 Kindness is contagious

If this article connected with you, consider tapping ❤️ or leaving a brief comment to share your thoughts!

Okay