Explain Codes LogoExplain Codes Logo

Wait until all promises complete even if some rejected

javascript
promise-engineering
error-handling
async-programming
Anton ShumikhinbyAnton Shumikhin·Nov 27, 2024
TLDR

When dealing with multiple promises in JavaScript and you need to wait for all of them to complete, irrespective of whether they passed or failed, we use Promise.allSettled(). This function bundles all your promises into a single one that resolves with an array showing the outcome of each promise.

// An array of our promises simulating calls to a couple of APIs const promises = [fetch('/api/one'), fetch('/api/two')]; Promise.allSettled(promises).then((results) => results.forEach(({status, value, reason}) => console.log(status, value || reason))); // Log 'em all!

The response for each outcome includes the status ('fulfilled' or 'rejected'), the value (if fulfilled), or reason (if rejected).

Diving deep with Promise.allSettled()

In this section, we extend beyond just using Promise.allSettled, dive deeper into handling the outcomes, and cater to environments where it might not be available yet.

Postprocessing the results of allSettled

Arguably, after you've received the results from Promise.allSettled(), you will want to separate the successful operations from the faltered. You can achieve this by filtering the outcomes based on the status property.

// That's how you separate the adults from the kids. Promise.allSettled(promises).then((results) => { const successfulPromises = results.filter(result => result.status === 'fulfilled'); const failedPromises = results.filter(result => result.status === 'rejected'); // Now, you can separately handle successful and failed promises. });

Fulfilling promises in ES5

Promise.allSettled may not exist in environments that don't support ES2020 or later. However, you can circumvent this by using a polyfill or create a custom settle method to mimic Promise.allSettled behavior.

// Who needs external promise libraries when you can do this in vanilla JavaScript?! 🍦 if (!Promise.allSettled) { Promise.allSettled = promises => Promise.all( promises.map(promise => promise.then(value => ({ status: 'fulfilled', value })) .catch(reason => ({ status: 'rejected', reason })))); }

With this polyfill, older environments can also use Promise.allSettled to ensure all promises are awaited.

Nailing error handling

Another pattern beyond Promise.allSettled is to catch errors for each promise individually using a reflect function. This way, you ensure no promise is left unattended and each can resolve or reject gracefully to aggregate the results.

// Caught you, error! Trying to sneak through, eh? function reflect(promise) { return promise.then( value => ({ status: 'fulfilled', value }), reason => ({ status: 'rejected', reason }) ); } // Wrangle all those promises together Promise.all(promises.map(reflect)).then(results => { // All errors are belong to us });

Beyond waiting - handling outcomes

Just waiting for all promises to settle won't cut it. We need to adequately manage the outcomes for each promise. So, let's dig a bit deeper and learn how we can harness the detailed information provided by Promise.allSettled.

Enforcing a uniform promise interface

By insisting that each promise conforms to a consistent interface (i.e., an object with a status and either value or reason), we can conveniently abstract the error handling and process the results predictably.

// Here, we make all promises promise to behave the same way. adjustPromises(promises) { return promises.map(promise => reflect(promise)); } // All promises are equal, but some promises are more equal than others. const adjustedPromises = adjustPromises([fetch(url1), fetch(url2)]); Promise.allSettled(adjustedPromises).then(results => { /* Process results */ });

Keeping out external libraries

The promise management features in modern JavaScript environments have considerably reduced the reliance on external promise libraries. Since Promise.allSettled is natively available, it often negates the need for additional dependencies for promise management in your projects.

Simple over complex error handling

Sometimes, simpler is better, aligning nicely with the KISS (Keep It Simple, Stupid) principle. Instead of developing complex error handlers, defaulting to a base case for errors can often suffice. In essence, if something ain't broke, don't complicate it.

// The code might fail, but our humor won't. Promise.allSettled(promises).then(results => { results.forEach(result => { const data = result.status === 'fulfilled' ? result.value : undefined; // Undefined is our new black. // Do magic with data, but remember, error cases are treated as undefined. }); });

Handling network failures elegantly

When dealing with network requests being graceful in failure management is vital. Possible scenarios you need to factor in may include slow network connections, request timeouts, and server issues - all unpredictable and fun parts of any developer's life.

// No more waiting for snail-paced APIs function fetchWithTimeout(url, timeout = 1000) { return new Promise((resolve, reject) => { setTimeout(() => reject(new Error('timeout')), timeout); // Remember, patience is a virtue. fetch(url).then(resolve, reject); }); } // Let's tackle all these promises together, shall we? Promise.allSettled(promises.map(fetchWithTimeout)).then(handleAllPromises);