Implementing 'N' Async Tasks in a Race in JavaScript



Running N asynchronous tasks in a race means initiating multiple tasks simultaneously and resolving the overall operation as soon as the first task completes. This approach is useful when you're interested in the quickest result, such as when making requests to multiple servers and using the fastest response.


What is Race Execution?

Race execution involves running multiple tasks at the same time and resolving or rejecting as soon as the first task completes. The rest of the tasks are ignored once the first one finishes, whether it resolves or rejects.

Real Interview Insights

Interviewers might ask you to:

  • Implement a function to execute an array of asynchronous tasks in a race.
  • Ensure that the race finishes as soon as the first task completes.
  • Handle the results and errors from the first completed task.

Implementing runTasksInRace Function

Here’s an implementation of a function to run N async tasks in a race:

function runTasksInRace(tasks) {
  return Promise.race(tasks.map(task => task()));
}
Explanation:
  • Mapping Tasks: Use Array.prototype.map to create an array of task promises.
  • Promise.race: Use Promise.race to race all the promises and resolve or reject as soon as the first one completes.

Practical Examples

Consider an example with async tasks:

const tasks = [
  () => new Promise(resolve => setTimeout(() => resolve('Task 1'), 1000)),
  () => new Promise(resolve => setTimeout(() => resolve('Task 2'), 500)),
  () => new Promise(resolve => setTimeout(() => resolve('Task 3'), 300))
];
 
runTasksInRace(tasks).then(result => {
  console.log(result); // Output: 'Task 3'
});

Handling Edge Cases

  1. Empty Task Array: Handle cases where the array of tasks is empty.
  2. Error Handling: Consider how to handle a situation where the first task to complete rejects.

Enhanced Implementation with Error Handling

function runTasksInRace(tasks) {
  if (tasks.length === 0) {
    return Promise.reject(new Error('No tasks provided'));
  }
 
  return Promise.race(tasks.map(task =>
    task().catch(error => {
      console.error(`Error in task: ${error.message}`);
      throw error;
    })
  ));
}
 
// Example usage with error handling
const tasks = [
  () => new Promise(resolve => setTimeout(() => resolve('Task 1'), 1000)),
  () => new Promise((resolve, reject) => setTimeout(() => reject(new Error('Task 2 failed')), 500)),
  () => new Promise(resolve => setTimeout(() => resolve('Task 3'), 300))
];
 
runTasksInRace(tasks)
  .then(result => {
    console.log(result); // Output: 'Task 3'
  })
  .catch(error => {
    console.error(error.message); // If Task 2 finishes first, Output: 'Task 2 failed'
  });

Use Cases for Race Execution

  1. Quickest Response: Use the result of the fastest task, such as fetching data from multiple servers.
  2. Timeout Implementation: Combine Promise.race with a timeout to ensure a task doesn't take too long.
  3. Fallback Mechanisms: Implement fallback strategies where you race primary and backup operations.