Learning Objectives

  • Understand how Promise.race() works
  • Learn when to use Promise.race()
  • Implement timeout patterns
  • Master racing strategies

What is Promise.race()?

Promise.race() returns a Promise that settles as soon as any of the input Promises settles (either fulfills or rejects).

Promise.race([promise1, promise2, promise3])
  .then(result => {
    // Result from the FIRST settled Promise
  })
  .catch(error => {
    // Error from the FIRST rejected Promise
  });

Basic Example

const slow = new Promise(resolve => 
  setTimeout(() => resolve('Slow'), 3000)
);

const fast = new Promise(resolve => 
  setTimeout(() => resolve('Fast'), 1000)
);

Promise.race([slow, fast])
  .then(result => {
    console.log(result); // "Fast" (after 1 second)
  });

Common Use Cases

1. Request Timeout

function timeout(ms) {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Timeout')), ms);
  });
}

function fetchWithTimeout(url, ms) {
  return Promise.race([
    fetch(url),
    timeout(ms)
  ]);
}

fetchWithTimeout('/api/data', 5000)
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Request failed or timed out:', error));

2. Fastest Server Wins

const servers = [
  'https://api1.example.com/data',
  'https://api2.example.com/data',
  'https://api3.example.com/data'
];

Promise.race(servers.map(url => fetch(url)))
  .then(response => response.json())
  .then(data => console.log('Got data from fastest server:', data))
  .catch(error => console.error('All servers failed:', error));

3. User Action vs Auto-Save

function waitForUserAction() {
  return new Promise(resolve => {
    document.getElementById('saveBtn').onclick = () => {
      resolve('manual');
    };
  });
}

function autoSave(delay) {
  return new Promise(resolve => {
    setTimeout(() => resolve('auto'), delay);
  });
}

Promise.race([
  waitForUserAction(),
  autoSave(30000) // 30 seconds
])
  .then(saveType => {
    console.log(`Saving (${saveType})...`);
    saveData();
  });

Promise.race() vs Promise.all()

Feature Promise.race() Promise.all()
Resolves when First Promise settles All Promises fulfill
Rejects when First Promise rejects Any Promise rejects
Result Single value Array of values
Use case Timeouts, fastest wins Parallel operations

Advanced Patterns

Retry with Timeout

async function fetchWithRetryAndTimeout(url, retries = 3, timeout = 5000) {
  for (let i = 0; i < retries; i++) {
    try {
      const result = await Promise.race([
        fetch(url),
        new Promise((_, reject) => 
          setTimeout(() => reject(new Error('Timeout')), timeout)
        )
      ]);
      return await result.json();
    } catch (error) {
      console.log(`Attempt ${i + 1} failed:`, error.message);
      if (i === retries - 1) throw error;
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  }
}

fetchWithRetryAndTimeout('/api/data')
  .then(data => console.log(data))
  .catch(error => console.error('All attempts failed:', error));

Cache Race Pattern

function fetchWithCache(url) {
  const cachePromise = caches.match(url);
  const networkPromise = fetch(url).then(response => {
    // Update cache in background
    caches.open('v1').then(cache => cache.put(url, response.clone()));
    return response;
  });
  
  return Promise.race([cachePromise, networkPromise])
    .then(response => {
      if (!response) return networkPromise;
      return response;
    });
}

First Success Pattern

function raceUntilSuccess(promises) {
  return Promise.race(
    promises.map(p => 
      p.then(
        value => Promise.resolve(value),
        error => new Promise(() => {}) // Never resolves on error
      )
    )
  );
}

raceUntilSuccess([
  fetchFromServer1(),
  fetchFromServer2(),
  fetchFromServer3()
])
  .then(data => console.log('First successful result:', data));

Important Considerations

1. Other Promises Still Run

// Even though race settles after 1 second,
// the slow promise still completes after 3 seconds
Promise.race([
  new Promise(resolve => {
    setTimeout(() => {
      console.log('Fast done');
      resolve('Fast');
    }, 1000);
  }),
  new Promise(resolve => {
    setTimeout(() => {
      console.log('Slow done'); // Still logs!
      resolve('Slow');
    }, 3000);
  })
])
.then(result => console.log('Winner:', result));

// Output:
// Winner: Fast (after 1s)
// Slow done (after 3s)

2. Empty Array Behavior

// Promise.race([]) never settles!
Promise.race([])
  .then(() => console.log('This never runs'))
  .catch(() => console.log('This never runs either'));

3. Rejection Wins Too

Promise.race([
  new Promise((_, reject) => 
    setTimeout(() => reject(new Error('Fast fail')), 100)
  ),
  new Promise(resolve => 
    setTimeout(() => resolve('Slow success'), 1000)
  )
])
.then(result => console.log(result))
.catch(error => console.error(error.message)); // "Fast fail"

Real-World Example: Image Loading

function loadImageWithTimeout(url, timeout = 5000) {
  return Promise.race([
    new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => resolve(img);
      img.onerror = () => reject(new Error('Failed to load image'));
      img.src = url;
    }),
    new Promise((_, reject) => 
      setTimeout(() => reject(new Error('Image load timeout')), timeout)
    )
  ]);
}

loadImageWithTimeout('photo.jpg', 3000)
  .then(img => document.body.appendChild(img))
  .catch(error => console.error('Image load failed:', error.message));

Key Takeaways

  • Promise.race() settles with the first settled Promise
  • ✅ Perfect for implementing timeouts
  • ✅ Use for fastest wins scenarios
  • ✅ Other Promises continue running even after race settles
  • ✅ Can reject if the fastest Promise rejects
  • ✅ Empty array never settles

Next Steps

Next, we'll learn about Promise.allSettled() for handling mixed results!