Wait for the Fastest JavaScript Promise to Be Fulfilled with Promise.any()

Marius Schulz
InstructorMarius Schulz

Share this video with your friends

Send Tweet
Published 2 years ago
Updated a year ago

The Promise.any() method accepts an array (or any other iterable) of promises as a parameter. It returns a Promise object that is fulfilled with the value of the first input promise to fulfill:

  • As soon as any input promise is fulfilled, the returned Promise object is fulfilled with that value.
  • If all input promises are rejected, the returned Promise object is rejected with an AggregateError which has an errors property containing an array of all rejection reasons.

Promise.any() can be used to race multiple promises against each other and find the first promise to be fulfilled.

Please note that at the moment, the Promise.any() method is only implemented in Firefox Nightly. Make sure to use a polyfill in your application to make it work across browsers.

Marius Schulz: [0:00] Once again, we are working with a list of "Star Wars" films. This is the demo application you've seen in previous lessons. We're still using the queryAPI() function to talk to the Star Wars API. When I go to the browser and refresh the page, we're fetching a list of films from starwars.egghead.training.

[0:19] A real-world API might be unavailable every now and then for various technical reasons. Let's see how our application behaves if we cannot reach egghead.training.

[0:29] I'm using Firefox here, so I'm going to open the Firefox Developer Tools. I'm going to head over to the Network tab, and I'm going to click this little Cancel icon right here that says Request Blocking. Firefox lets us block any request whose URL matches a specific string. I'm going to block all requests to egghead.training to simulate that the Star Wars API is unavailable.

[0:51] When I refresh the page again, you can see that our API request is now unsuccessful and that we don't see any films anymore. This is because the Star Wars API is a single point of failure for our application. If it's unavailable, we cannot show any data.

[1:07] I want to use the Promise.any() method to make our application more resilient against occasional API downtimes. Let's take a look at how Promise.any works.

[1:16] Let's go back to the console, and let's also detach this window just so we have a little more space available. I'm also going to hide the warnings and open the multi-line editor. Let me go ahead and create a bunch of test promises for us. I want several promises that have been rejected, and one that has been fulfilled.

[1:37] Let's pass those promises through the Promise.any method. Promise.any accepts a bunch of input promises and returns us a new promise. As the method name suggests, that new promise is fulfilled if any of the input promises is fulfilled. It is only rejected if all of the input promises are rejected and not a single one is fulfilled.

[1:58] Promise.any is useful in scenarios where we only need one of the input promises to succeed, rather than all of them. Let's go ahead and create a promise chain. I'm going to add a fulfillment handler, as well as a rejection handler. All right, let's run this code and see what happens.

[2:23] We can see that our promise was fulfilled. This is because out of the three promises that we were passing to Promise.any, one of them was ultimately fulfilled with a value of 42. That's the value that we're getting here. Notice that if there's more than one promise that is fulfilled, we still only get back the first fulfillment value. The other fulfillment values are simply ignored.

[2:48] Let's now shift gears and take a look at what happens if all input promises are being rejected. I'm going to remove the last two promises from the array, and I'm going to rerun our example. This time our rejection handler is executed, and it receives an AggregateError as its parameter.

[3:04] The AggregateError contains the rejection reasons of all of our input promises, and we can access them using the errors property. Let's clear the output and run this again. This time, we can see an array of errors printed to the console. These are all the rejection reasons in the same order that we specified our promises in.

[3:25] Now that you know how the Promise.any method works, let's go back to our application and use it in a practical way. We want to avoid the single point of failure of the Star Wars API. I'm going to rename this constant to API_URL_1, and I'm going to add a URL to another API that provides the same data.

[3:48] Let's go ahead and refactor our queryAPI function. We are currently hard-coding API_URL_1. Let's create a new function called query() that accepts a rootURL and an endpoint. Let's take our fetching logic and move it inside that function. Instead of API_URL_1, we are now going to use rootURL.

[4:10] Back in our queryAPI function, we now want to use Promise.any, and we want to call our query function twice, once with API_URL_1 and our endpoint, and once with API_URL_2. OK, this is the moment of truth. Let's go back to our browser and give the page a refresh. There we go. We see our list of films again.

[4:35] If we go back to the network tab, we can see that the request through egghead.training is still failing, and this is because our request blocking is still on. Whatever request we want to make, we now make it against two APIs. As long as one of the responses comes back successfully, we're good. Of course, if both APIs are unavailable at the same time, we're back to square one.

[4:57] We can simulate this by blocking requests to the other API as well. If we give this a refresh, you'll see that we now see an error message again. Let's now switch back to the Console tab and take a look at the error message that we logged to the console. All of the promises that we passed through Promise.any ended up being rejected. This is why we're getting an AggregateError.

[5:19] However, "No Promise in Promise.any was resolved" might not be the error message that we want to have. Let's attach a rejection handler and change the error message to a better one. I'm going to return a promise that has been rejected with a custom error message. If we refresh the page one more time, we now get a better message logged to the console.

[5:43] Here's the cool thing. Let me go ahead and collapse our code and let's take a look at our promise chain. We haven't changed this piece of code at all. Every change we've made has been within the queryAPI function and the setup code. This means that callers of the queryAPI function don't even have to know that we're now talking to two APIs rather than just a single one. All of this logic has been encapsulated and is fully opaque to the caller.

[6:10] Finally, we might have even made our application a little bit faster. Let's go back to the network tab and let's disable request blocking. Let me go ahead and give this page a refresh.

[6:22] You can see that the first response took 675 ms, whereas the second one took 901. That's perfectly normal because response times vary. Let's refresh the page again. This time, the second response came back much more quickly than the first one. Previously, we would have had to wait 646 ms for a response, whereas now, we already get one after 356 ms.

[6:49] This is a nice speedup. Obviously, it only works if the two APIs return the same data. Otherwise, we would have just introduced the race condition to our application.

[7:00] There you go. Use the Promise.any method, when you have a bunch of promises and you're only interested in the first fulfillment value, rather than all of them.

~ 2 years ago

...Is 42 a Hitchhikers Guide to the Galaxy reference? 😂

Marius Schulz
Marius Schulzinstructor
~ 2 years ago

...Is 42 a Hitchhikers Guide to the Galaxy reference? 😂

Possibly! I can neither confirm nor deny. 😉