# Dealing with Promises In an Array with async/await
Promises and async/await
is a welcomed addition to the newer versions of
JavaScript. If you are not using it yet and are trapped in the callback hell,
you might want to check it out and start using it already. Believe me, it's
awesome! The MDN
docs
would be a good place to start, and
CSS-Tricks has a good
article on it as well.
But it can be a little bit tricky when using async/await
to deal with a
collection of promises. Thankfully, here is my cheatsheet for dealing with
them, created based on my experience.
p.s. No external libraries! 😉
Now, let's get started! Imagine we have the following asynchronous functions:
const resolveInTwoSeconds = () => {
return new Promise((resolve) => {
setTimeout(() => resolve(2), 2000);
})
};
const resolveInThreeSeconds = () => {
return new Promise((resolve) => {
setTimeout(() => resolve(3), 3000);
})
};
const resolveInFiveSeconds = () => {
return new Promise((resolve) => {
setTimeout(() => resolve(5), 5000);
})
};
# 1. Wait for all promises to complete with Promise.all
Promise.all
accepts an array of promises and returns a new promise that
resolves only when all of the promises in the array have been resolved. The
promise resolves to an array of all the values that the each of the promise
returns.
(async function() {
const asyncFunctions = [
resolveInTwoSeconds(),
resolveInThreeSeconds(),
resolveInFiveSeconds()
];
const results = await Promise.all(asyncFunctions);
// outputs `[2, 3, 5]` after five seconds
console.log(results);
})();
# 2. Wait for at least one promise to complete with Promise.race
Promise.race
accepts an array of promises and returns a new promise that
resolves immediately when one of the promises in the array have been resolved,
with the value from that promise.
(async function() {
const asyncFunctions = [
resolveInTwoSeconds(),
resolveInThreeSeconds(),
resolveInFiveSeconds()
];
const result = await Promise.race(asyncFunctions);
// outputs `2` after two seconds
console.log(result);
})();
# 3. Wait for all promises to complete one-by-one
There's no native methods on Promise
class that can do this quickly, but we
can make use of Array.prototype.reduce
method to achieve the goal.
(async function() {
const asyncFunctions = [resolveInTwoSeconds, resolveInThreeSeconds, resolveInFiveSeconds];
// outputs 2 after 2 seconds
// outputs 3 after 5 seconds
// outputs 5 after 8 seconds
await asyncFunctions.reduce(async (previousPromise, nextAsyncFunction) => {
await previousPromise;
const result = await nextAsyncFunction();
console.log(result);
}, Promise.resolve());
})();
This is less straight-forward than the previous implementations, but I am going to write a separate post to explain this. Let's keep this post just for quick cheatsheets 😉.
# 4. Run async functions batch-by-batch, with each batch of functions executed in parallel
This is really helpful if you want to avoid hitting the rate limit of some API
service. This makes use of the same concept in #3, where we have an array of
promises resolved sequentially, combined with a two-dimensional array of
promises and the use of Promise.all
.
The key here is to build the collection of async functions in a two-dimensional
array first. Once we have that, we can iterate over each collection of async
functions and execute them in parallel, and use Promise.all
to wait for each
of those functions to complete. Until all of the promises in the current batch
resolve, we are not going to process the next batch.
Here's the full implementation of the above concept:
(async function() {
const asyncFunctionsInBatches = [
[resolveInTwoSeconds, resolveInTwoSeconds],
[resolveInThreeSeconds, resolveInThreeSeconds],
[resolveInFiveSeconds, resolveInFiveSeconds],
];
// Outputs [2, 2] after two seconds
// Outputs [3, 3] after five seconds
// Outputs [5, 5] after eight seconds
await asyncFunctionsInBatches.reduce(async (previousBatch, currentBatch, index) => {
await previousBatch;
console.log(`Processing batch ${index}...`);
const currentBatchPromises = currentBatch.map(asyncFunction => asyncFunction())
const result = await Promise.all(currentBatchPromises);
console.log(result);
}, Promise.resolve());
})();
Keep in mind that I'm building the batches of async functions through hard-coding here. In a real application, you might have a dynamic length of array returned from an API call or the likes, so you will have to split them yourselves. A quick implementation for this task:
const splitInBatch = (arr, batchSize) => {
return arr.reduce((accumulator, element, index) => {
const batchIndex = Math.floor(index / batchSize);
if (Array.isArray(accumulator[batchIndex])) {
accumulator[batchIndex].push(element);
} else {
accumulator.push([element]);
}
return accumulator;
}, []);
}
// outputs [[1, 2, 3], [4, 5, 6]]
console.log(splitInBatch([1, 2, 3, 4, 5, 6], 3));
Or, you can also opt for libraries such as lodash
to help you with this task.
import chunk from 'lodash.chunk';
// outputs [[1, 2, 3], [4, 5, 6]]
console.log(chunk([1, 2, 3, 4, 5, 6], 3));
# 5. Bonus Tip: Do not pass an async function to forEach
Remember, the difference between Array.prototype.map
and
Array.prototype.forEach
is that the latter does not return the result of each
iteration. If we pass async
functions to forEach
, we have no way of
retrieving the returned promise to do anything useful with it. Unless you want
to fire the async function and forget about it, passing async functions to
forEach
is never something you want to do.
# Conclusion
There you go! That is all 5 cheatsheets on what to do and not to do with an array of Promises. I hope this has been useful to you all 😁, and please, please, let me know in the comments section if there's anything I ought to improve upon.
See you again!