When to Use Promise.race in JavaScript: A Practical Guide with Examples

JavaScript promises are an essential tool for managing asynchronous operations. Among the many methods provided by the Promise API, one particularly useful but often overlooked method is Promise.race. This article will explore when and how to use Promise.race, with practical examples, including a real-world use case for DNS resolution with a timeout.

What is Promise.race?

Promise.race is a method in the Promise API that takes an iterable of promises and returns a new promise. The returned promise resolves or rejects as soon as any of the promises in the iterable resolve or reject. This behavior makes Promise.race an excellent tool for scenarios where you want to take action based on the fastest response, regardless of whether it succeeded or failed.

Syntax:

Promise.race(iterable);
  • iterable: An array or any iterable of promises.

Common Use Cases for Promise.race

  1. Timeout Management for Asynchronous Operations
  2. Prioritizing Faster Responses
  3. Fallback Mechanisms

Let’s break these use cases down with practical examples.

1. Timeout Management for Asynchronous Operations

One of the most common uses of Promise.race is to impose a timeout on an asynchronous operation. For example, when performing network requests, you might want to abort the request if it takes too long to complete.

Here’s how you can use Promise.race to implement a timeout:

function fetchWithTimeout(url, timeout) {
  const fetchPromise = fetch(url);
  const timeoutPromise = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Request timed out')), timeout)
  );

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

fetchWithTimeout('https://api.example.com/data', 5000)
  .then((response) => console.log('Response received:', response))
  .catch((error) => console.error('Error:', error.message));

In this example, Promise.race ensures that the first promise to settle (either the fetchPromise or timeoutPromise) determines the outcome. If the network request takes longer than 5 seconds, the timeout promise rejects, and the error is handled accordingly.

2. Prioritizing Faster Responses

In some cases, you may have multiple sources of data, such as APIs or caches, and you want to use the result from the source that responds the fastest. Promise.race can help you accomplish this.

const apiRequest = fetch('https://api.example.com/data');
const cacheRequest = new Promise((resolve) => {
  setTimeout(() => resolve({ data: 'Cached data' }), 100); // Simulated cache
});

Promise.race([apiRequest, cacheRequest])
  .then((result) => console.log('Fastest response:', result))
  .catch((error) => console.error('Error:', error));

Here, Promise.race gives you the fastest response, whether it’s from the cache or the API. This is especially useful for improving user experience by minimizing perceived latency.

3. Fallback Mechanisms

Promise.race can also be used to implement fallback strategies. For example, if a primary operation fails or takes too long, you can fall back to a secondary operation.

function fetchWithFallback(primaryUrl, secondaryUrl, timeout) {
  const primaryRequest = fetchWithTimeout(primaryUrl, timeout);
  const fallbackRequest = fetch(secondaryUrl);

  return Promise.race([primaryRequest, fallbackRequest]);
}

fetchWithFallback('https://primary.example.com', 'https://fallback.example.com', 3000)
  .then((response) => console.log('Response:', response))
  .catch((error) => console.error('Error:', error));

This ensures that your application can still provide data to the user, even if the primary source is unavailable or slow.

Real-World Example: DNS Resolution with a Timeout

Let’s apply Promise.race in a real-world scenario. Suppose you are resolving a hostname to an IP address and want to avoid waiting indefinitely if the DNS server doesn’t respond in a timely manner.

Here’s an implementation using Node.js’s dns module:

import * as dns from 'dns';

/**
 * Resolves the IP address for a given hostname.
 * @param hostname The hostname to resolve.
 * @param family The address family (4 for IPv4, 6 for IPv6). Defaults to 4.
 * @param timeout The timeout in milliseconds for the resolution. Optional.
 * @returns A promise that resolves with the IP address.
 */
export const resolveHostname = async (
  hostname,
  family = 4,
  timeout
) => {
  return new Promise((resolve, reject) => {
    const lookupPromise = new Promise((resolveLookup, rejectLookup) => {
      dns.lookup(hostname, { family }, (err, address) => {
        if (err) {
          rejectLookup(err);
          return;
        }
        resolveLookup(address);
      });
    });

    if (timeout) {
      const timeoutPromise = new Promise((_, rejectTimeout) =>
        setTimeout(
          () => rejectTimeout(new Error('DNS resolution timeout')),
          timeout
        )
      );

      Promise.race([lookupPromise, timeoutPromise])
        .then(resolve)
        .catch(reject);
    } else {
      lookupPromise.then(resolve).catch(reject);
    }
  });
};

Usage:

resolveHostname('example.com', 4, 5000)
  .then((ip) => console.log('Resolved IP:', ip))
  .catch((error) => console.error('Error:', error.message));

In this example, Promise.race ensures that the DNS resolution either completes within the specified timeout or rejects with a timeout error. This approach is crucial for building robust and responsive applications, especially in environments with unreliable network conditions.

When NOT to Use Promise.race

While Promise.race is powerful, it may not be the right choice in all situations. For instance:

  1. When You Need All Results: If you need to wait for all promises to resolve or reject, use Promise.all instead.
  2. Error Masking: Promise.race does not provide detailed error handling for all promises — it only captures the first one to settle. If this is a concern, consider alternative approaches.

Conclusion

Promise.race is a versatile and powerful method for managing asynchronous operations in JavaScript. From enforcing timeouts to prioritizing faster responses and implementing fallback mechanisms, it enables developers to create more efficient and user-friendly applications.

By incorporating Promise.race into your toolkit, you can build resilient code that handles uncertainties in the real world, such as network latency, unreliable APIs, or slow DNS lookups. Use the examples provided in this article to get started, and experiment with how Promise.race can simplify your asynchronous workflows.

Key Takeaway: When working with asynchronous operations where timing is critical, Promise.race is your go-to method for gaining control over which promise wins the race.