Understanding Promise.race - A Naïve Approach to Faster Resolutions
Published onSometimes, the fastest solution isn’t the best one—it’s just the first one to finish.
In the world of JavaScript, Promises offer respite from the callback pyramid of doom. They are a powerful tool for managing asynchronous code. With promises, one can handle actions such as fetching data, performing computational heavy operations, or interacting with other services, without going through the hustle of debugging multiple layers of callbacks.
For most situations, this is where the Promise “magic” of JavaScript stops. However, what do you do when you want to get data from just one of various services (say the price of an asset), but you aren’t sure which service would work the fastest? That is where Promise.race
comes in.
Promise.race
is a utility function that takes an array of promises and resolves or rejects as soon as the first one settles. It’s simple yet incredibly versatile, lending itself to a range of scenarios where responsiveness is key.
This blog post is part of a series exploring Promise.race
. We’ll begin by demystifying how it works, examine its potential use cases, and weigh its advantages and disadvantages. Think of it as your crash course in racing promises without breaking a sweat.
In this first post, we’ll introduce the concept of Promise.race
and set the stage for understanding its capabilities. In the posts to follow, we’ll dive deeper into specific use cases like:
- Writing cancellable actions,
- Handling timeouts,
- Implementing fallback strategies,
- And much more.
Let’s start by unraveling the mystery of Promise.race
—a deceptively simple tool that just might change the way you approach asynchronous programming.
Promise.race
?
What is At it’s core Promise.race
is a method from the JavaScript Promise API that takes an iterable (usually an array
) of promises and does as the name suggests: “initiates a race
between multiple promises, resolving or rejecting as soon as the first one settles—regardless of the outcome.”
Syntax
Promise.race(iterable);
Please note that the Promise settles whichever Promise resolves first, whether it is successful or fails. Essentially, it returns the result of the promise that settles first—whether it succeeds or fails.
Pop the Hood - It’s a race afterall
To truly appreciate the power of Promise.race, we need a simple example;
const slowPromise = new Promise((resolve) => setTimeout(resolve, 1000, 'Slow'));
const fastPromise = new Promise((resolve) => setTimeout(resolve, 500, 'Fast'));
Promise.race([slowPromise, fastPromise]).then((result) => {
console.log(result); // Output: "Fast"
});
Here’s what’s happening:
- Two promises are created: one resolves after 1 second, and the other resolves after 0.5 seconds.
Promise.race
evaluates both, but it only cares about which one settles first.- Since the second promise (
fastPromise
) resolves in 0.5 seconds, it wins the race, and the final result is"Fast"
.
A Race That Fails
Promises don’t always resolve or “succeed” - they can also reject. In this case, if the fast promise would have rejected before the slow one, the race is settled as a rejection. See an example of this below;
const slowPromise = new Promise((resolve) => setTimeout(resolve, 1000, 'Slow'));
const errorPromise = new Promise((_, reject) => setTimeout(reject, 500, 'Error'));
Promise.race([slowPromise, errorPromise])
.then((result) => {
console.log('Resolved with:', result);
})
.catch((error) => {
console.error('Rejected with:', error); // Output: "Rejected with: Error"
});
Potential Use Cases
Before diving into potential use cases, it’s worth considering why you might race two promises, especially if one could fail. However, Promise.race
’s simplicity and usecase belies in it’s potential. It’s not just about seeing which promise is fastest; it’s about enhancing efficiency and responsiveness in your code. Promise.race
shines in scenarios like implementing fallbacks, timing out slow operations, or any situation that prioritizes speed over reliability.
Now we’re at the starting line of the race: the potential use cases. There are many ways to harness its power, but for now, we’ll stick to the highlights, listing them out to give you a sense of its breadth.
In future posts, we’ll deep-dive into each of these use cases, complete with examples and practical implementations. Consider this your track map—save it for reference, as I’ll be linking each future post back to its corresponding section here.
Enough talk, let’s line up the contenders!
- Cancellable Actions
- Abort ongoing operations when they’re no longer needed.
- Timeouts for Slow Operations
- Prevent long-running tasks from hanging indefinitely by imposing time limits.
- Fallback Strategies
- Use the first available result when querying multiple services.
- Quickest Response from Redundant Sources
- Always rely on the fastest response from redundant or backup systems.
- Dynamic Loading
- Load resources like scripts or stylesheets from multiple URLs, using the first to respond.
- First Event Listener to Trigger
- Respond to whichever event (e.g., click or keypress) happens first.
- Polling with Early Exit
- Monitor resources periodically but exit early if certain conditions are met.
- Competition Between Background Tasks
- Use the result of whichever background task completes first.
- Simulating Concurrent Actions in Tests
- Simulate scenarios where multiple asynchronous actions are racing.
- Graceful Degradation
- Ensure users receive a fallback experience if the primary task fails or is too slow.
- Real-Time Messaging
- Optimize responsiveness by racing between multiple message-delivery mechanisms.
With these use cases in mind, it’s clear that Promise.race
is a powerful tool in your asynchronous toolkit, capable of streamlining operations and adding agility to your code. Whether you’re racing for the fastest result or ensuring your app doesn’t get bogged down by slow operations, Promise.race
can make a significant impact.
As we move forward, I’ll dive deeper into each of these use cases in future posts, exploring them in detail with practical examples and real-world scenarios. Consider this just the beginning—stay tuned for a deeper dive into how Promise.race
can truly elevate your asynchronous programming. If you have some more that I haven’t listed, please feel free to reach out.
Now, let’s shift gears and explore the advantages and disadvantages of using Promise.race
to help you determine when it’s the perfect fit, and when it might trip you up.
Promise.race
Advantages of Using - Improved Efficiency: Allows your code to get to the result faster, cutting through the noise of waiting for multiple tasks to finish. When speed is a priority, it’s a game changer.
- Better Resource Management: can prevent unnecessary operations or background tasks from running too long. It stops the moment a promise finishes, letting you conserve resources and move on quicker.
- Versatile: From handling timeouts in network requests to picking the fastest response between two services,
Promise.race
can be used in many scenarios, saving you from building complex conditional logic. It’s your Swiss army knife for asynchronous challenges. - Simpler Code: Instead of managing promises manually with
then
andcatch
,Promise.race
allows you to condense the logic into one neat line of code. No more convoluted promise chains—just clear, concise code that does the job.
Disadvantages and Gotchas
- Unreliable Results:
Promise.race
is like gambling. You’re betting that the fastest promise will be the correct one. But if the first promise to settle is a rejection, your code will handle that failure—no matter how important the other promises might have been. If you’re looking for stability, this might not be the tool for the job. - No Control Over Which Promise Wins: Think of it like a race with no rules—you can’t control which promise crosses the finish line first. The promise that resolves or rejects first takes the crown, but if the slowest promise holds the key to your data, you might end up losing the race.
- No Guarantee of Reliability: Speed can sometimes lead to accidents, and in the case of
Promise.race
, speed often comes at the cost of reliability. You might get the fastest result, but it could be incomplete or even wrong, especially if the promise that wins isn’t the one you can trust. - Hard to Debug: If something goes wrong in a race, it can be tough to pinpoint the problem. With
Promise.race
, tracking down issues can be trickier because multiple promises are at play. If something goes awry, the first promise to resolve (whether successfully or not) dictates the outcome, making the issue harder to trace back.
Conclusion
Promise.race
is the ultimate tool when you need to prioritize speed and responsiveness in your code. It’s like having a turbo boost for your asynchronous operations—whether it’s handling timeouts, creating fallbacks, or dynamically loading resources. However, as with any fast-paced race, there are risks involved. The tool’s tendency to resolve on the fastest promise means it may not always give you the most reliable result, and that’s something to consider when deciding whether to use it.
Ultimately, Promise.race
shines when speed is your main goal, but like any race, it’s important to know when to put on the brakes. Stay tuned for future posts where we’ll dig deeper into how to harness this powerful function in specific use cases!
If you enjoyed this post, I regularly write about development, sharing insights and practical tips on JavaScript, web development, and more. On the side, I’m a trader and build trading tools, discussing strategies and insights on how to improve trading performance.
You can explore more of my content on my website, and for updates on both my development and trading journey, feel free to follow me on Medium.
Let’s continue building, learning, and growing together!