Promises with Loops and Array Methods in JavaScript
Promises are objects that represent the eventual completion (or failure) of an asynchronous operation and allow you to handle the result of that operation when it completes. Loops are used to execute a block of code multiple times.
However, when working with asynchronous code, it’s important to understand how loops behave and how to use promises to control the flow of execution.
Usage with for..of loop
One common use case for using promises with loops is when you need to perform an operation multiple times, but each operation depends on the result of the previous one. In this case, you can use a loop to iterate over the data and a promise to control the flow of execution.
Let’s take a look at an example:
const items = [1, 2, 3, 4, 5];
function processItem(item) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Processing item ${item}`);
resolve(item);
}, Math.random() * 1000);
});
}
async function processItems() {
let result = 0
for (const item of items) {
result = await processItem(result + item);
}
console.log('All items processed');
}
processItems();
Executing this code will work sequentially as expected adding the result of the previous item to the current item. See the following output:
Processing item 1
Processing item 3
Processing item 6
Processing item 10
Processing item 15
All items processed
But, if waiting for the previous result is not required or the order of execution doesn’t matter, it is best not to wait for the results and remove the keywords async
and await
.
This way, the code can execute concurrently without blocking the main thread. See the following code and output:
function processItems() {
for (const item of items) {
processItem(item);
}
}
processItems();
console.log('All items sent for processing');
All items sent for processing
Processing item 4
Processing item 2
Processing item 5
Processing item 1
Processing item 3
Sometimes it is required to wait for all the concurrent tasks before returning from the function or exiting the code. Promise.all can be used in this case to wait for the result.
async function processItems() {
const promiseArray = []
for (const item of items) {
promiseArray.push(processItem(item));
}
const result = await Promise.all(promiseArray);
console.log(result);
}
processItems();
console.log('All items sent for processing');
All items sent for processing
Processing item 2
Processing item 4
Processing item 5
Processing item 3
Processing item 1
[ 1, 2, 3, 4, 5 ]
Usage of Promises with traditional for(let i=0;i<10;i++)
loops will also behave like the above examples.
Usage with Array Methods
Often there are times when an asynchronous operation needs to be carried out for each item of an array. Let’s take a look at the different ways of doing that.
Using forEach
We can use the forEach()
method to iterate over the array, and use Promises to execute the asynchronous operation for each object concurrently. Here's how it might look:
async function processItems() {
items.forEach((item) => {
processItem(item);
})
}
processItems();
console.log('All items sent for processing');
All items sent for processing
Processing item 2
Processing item 5
Processing item 1
Processing item 3
Processing item 4
As forEach
takes a function as an argument, each item is processed separately in the function. So even if the async
and await
keywords are added to the function, forEach
will still run the asynchronous code concurrently.
// still runs concurrently
async function processItems() {
items.forEach(async (item) => {
await processItem(item);
})
}
It’s recommended to use for..of
loop if it is required to wait for each promise to complete or to run the asynchronous operations sequentially for each item.
The promises can be executed sequentially by chaining the promises but it's generally not a good practice. See the following example for the promise chain trick.
function processItems() {
let chainedPromise = Promise.resolve();
items.forEach((item) => {
chainedPromise = chainedPromise.then(() => {
return processItem(item);
})
});
}
processItems();
console.log('All items sent for processing');
Using map
map
method behaves very similarly to forEach
with the difference being it allows to return a value for each item of an array. Let’s rewrite the above examples with map
.
function processItems() {
items.map((item) => {
processItem(item);
})
}
processItems();
console.log('All items sent for processing');
The above example runs concurrently to produce the following output.
All items sent for processing
Processing item 5
Processing item 2
Processing item 3
Processing item 4
Processing item 1
map
makes it easier for using Promise.all
as it allows us to return a value so that we can wait for the results of the promises.
Let’s return the promise from the map function and use it in a Promise.all
async function processItems() {
const promiseArray = items.map((item) => {
return processItem(item);
})
const result = await Promise.all(promiseArray);
console.log(result);
}
processItems();
console.log('All items sent for processing');
All items sent for processing
Processing item 1
Processing item 3
Processing item 5
Processing item 4
Processing item 2
[ 1, 2, 3, 4, 5 ]
The code still runs concurrently and then awaited for all the Promises to settle. The promise chain trick we saw previously can be used with map
if the Promises need to be executed sequentially.
async function processItems() {
let chainedPromise = Promise.resolve();
const promiseArray = items.map((item) => {
return chainedPromise = chainedPromise.then(() => {
return processItem(item)
});
})
const result = await Promise.all(promiseArray);
console.log(result);
}
processItems();
console.log('All items sent for processing');
All items sent for processing
Processing item 1
Processing item 2
Processing item 3
Processing item 4
Processing item 5
[ 1, 2, 3, 4, 5 ]
Error Handling
When working with Promises, there’s always the possibility of an error occurring. It’s important to handle these errors to prevent the program from crashing or behaving unexpectedly.
The following error is generally thrown when a Promise rejection error is not handled properly.
UnhandledPromiseRejectionWarning: Unhandled promise rejection ...
Handling with the .catch method
The errors should be handled with the .catch
method on a Promise object. Otherwise, the process will be terminated with a non-zero exit code.
The examples above for .forEach
and .map
can be modified to handle the errors with the .catch
method.
function processItem(item) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Processing item ${item}`);
Math.random() > 0.5
? resolve(item)
: reject(`error occurred for ${item}`);
}, Math.random() * 1000);
});
}
function processItems() {
items.forEach((item) => {
processItem(item).catch(err => {
console.log(err);
})
})
}
This will gracefully handle the errors and log them to the console. Otherwise, it would have crashed the program.
All items sent for processing
Processing item 5
Processing item 3
Processing item 4
error occurred for 4
Processing item 2
error occurred for 2
Processing item 1
error occurred for 1
Handling errors in Promise.all
If any promise is rejected from the promise array passed to Promise.all
, Promise.all
will also reject and throw an error. To fix this, again, every promise should end with a .catch
method.
async function processItems() {
const promiseArray = items.map((item) => {
return processItem(item)
.catch(err => console.log(err));
})
const result = await Promise.all(promiseArray);
console.log(result);
}
All items sent for processing
Processing item 2
Processing item 3
Processing item 1
error occurred for 1
Processing item 5
Processing item 4
error occurred for 4
[ undefined, 2, 3, undefined, 5 ]
Promise.allSettled
can also be used as an alternative, it will always resolve even if any or all of the promises throw an error.
async function processItems() {
const promiseArray = items.map((item) => {
return processItem(item);
})
const result = await Promise.allSettled(promiseArray);
console.log(result);
}
All items sent for processing
Processing item 3
Processing item 5
Processing item 2
Processing item 4
Processing item 1
[
{ status: 'rejected', reason: 'error occurred for 1' },
{ status: 'fulfilled', value: 2 },
{ status: 'fulfilled', value: 3 },
{ status: 'fulfilled', value: 4 },
{ status: 'rejected', reason: 'error occurred for 5' }
]
Handling errors using try/catch block
An alternative to .catch
method is using traditional try {...} catch() {...}
blocks. But this should only be used if the promise is awaited using the await
keyword. try/catch
will have no effect if the promise is not awaited.
function processItems() {
items.forEach(async (item) => {
try {
await processItem(item);
} catch(err) {
console.log(err);
}
})
}
Note that the following code would still throw an error because try/catch
block only handles promises awaited with the await
keyword.
function processItems() {
items.forEach((item) => {
try {
processItem(item); // error
} catch(err) {
console.log(err);
}
})
}
Conclusion
Working with Promises inside loops and array methods can be tricky for beginners. I hope this article helped you in grasping this concept.
Thank you for reading and see you at the next one!