DEV Community

Chidozie C. Okafor
Chidozie C. Okafor

Posted on • Originally published at doziestar.Medium on

JavaScript Under the Hood: `Promise.race`

Imagine entering your preferred eatery, where two cooks are in a competition to serve you the fastest. Although they are both talented and skillful cooks, their cooking speeds can differ according on the item they are preparing.

There’s a special rule in this restaurant: no matter how far along the other chefs are, you get to receive and eat the meal that the first chef finishes preparing. The second dish is never shown to you or given a taste; it is just put away. It’s all about who serves you first in this “race”.

The “Promise.race” Restaurant

In Javascript ther is a concept known as “Promise.race”. Imagine this as a competition between two or more promises, similar to our cooks, to see who can finish first.

Think of the pledges as chefs in our restaurant. They are both in the process of finishing a chore or cooking a food for you. The result of your dinner (or the code you receive) depends on the chef (or promise) who finishes first.

let chefA = new Promise((serve) => {
    setTimeout(serve, 500, 'Burger');
});

let chefB = new Promise((serve) => {
    setTimeout(serve, 100, 'Salad');
});
Enter fullscreen mode Exit fullscreen mode

chefA prepares a burger in 500 milliseconds, while chefB can whip up a salad in just 100 milliseconds.

Who Serves First?

In our special restaurant, you don’t get both dishes. You only get the dish of the chef who finishes first. This is where Promise.race comes into play

Promise.race([chefA, chefB]).then(dish => {
  console.log(`You got served: ${dish}`);
  // "You got served: Salad"
});
Enter fullscreen mode Exit fullscreen mode

Given our chefs’ preparation times, you’ll receive the salad, because chefB serves faster!

But What About the Buzzer?

We have a catch here at the restaurant. A loud siren will go off, indicating that you are free to leave without receiving a dish, if neither chef serves you within the allotted time.

In our code, this “buzzer” is a timeout. It ensures that procedures don’t take longer than necessary:

let chefSpecial = new Promise((serve) => {
    setTimeout(serve, 6000, 'Special Dish');
});

let buzzer = new Promise((_, kickOut) => setTimeout(() => kickOut('Buzzer Alert: Too slow!'), 5000));

Promise.race([chefSpecial, buzzer]).then(dish => {
    console.log(`You got served: ${dish}`);
}).catch(alert => {
    console.error(alert);
    // "Buzzer Alert: Too slow!"
});
Enter fullscreen mode Exit fullscreen mode

The buzzer (timeout) will sound initially, indicating that the meal preparation took longer than five seconds.

How we are using “Promise.race” at HubHub

The Promise.race Mechanism:

This is where things get interesting! Inside the loop, a race is set up between two promises using Promise.race:

  • The actual YouTube search: youtube.search(query, { type }).
  • A “buzzer” or timeout promise: This promise doesn’t resolve with any data; instead, it rejects with an error if the specified timeout (TIMEOUT) is reached without getting a response from the YouTube search.

Handling the Race Outcome:

  • Successful Search: If the YouTube search finishes first (before the timeout), the response is returned to the caller.
  • Timeout Reached: If the “buzzer” promise is the faster one (meaning the search took too long), it rejects with a “Request Timeout” error.
  • Rate-Limiting Error (Status 429): If YouTube indicates that you’re making requests too quickly (status code 429), the function will catch this error and retry the search.
async searchYoutube(query, type) {
    if (!validateInputs(query, type)) {
      throw new Error('Invalid inputs for search.');
    }

    let retries = 0;

    while (retries < MAX_RETRIES) {
      try {
        const response = await Promise.race([
          youtube.search(query, { type }),
          new Promise((_, reject) => setTimeout(() => reject(new Error('Request Timeout')), TIMEOUT)),
        ]);
        return response;
      } catch (error) {
        if (error.message === 'Request Timeout' || (error.response && error.response.status === 429)) {
          retries++;
          console.log(`Attempt ${retries} failed. Retrying...`);
        } else {
          Sentry.captureException(error);
          throw new HttpException(INTERNAL_SERVER_ERROR, 'Internal Server Error');
        }
      }
    }

    throw new Error('Failed to retrieve search results after maximum retries.');
  }
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
websilvercraft profile image
websilvercraft

I'm also using Promise.race to add timeouts to fetch calls, in production to make sure the calls don't remain hanged up to consume server's resources.