Reputation: 312
I have an array-like structure that exposes async methods. The async method calls contain try-catch blocks which in turn expose more async methods in the case of caught errors. I'd like to understand why forEach
doesn't play nicely with async
/await
.
let items = ['foo', 'bar', 'baz'];
// Desirable behavior
processForLoop(items);
/* Processing foo
* Resolved foo after 3 seconds.
* Processing bar
* Resolved bar after 3 seconds.
* Processing baz
* Resolved baz after 3 seconds.
*/
// Undesirable behavior
processForEach(items);
/* Processing foo
* Processing bar
* Processing baz
* Resolved foo after 3 seconds.
* Resolved bar after 3 seconds.
* Resolved baz after 3 seconds.
*/
async function processForLoop(items) {
for(let i = 0; i < items.length; i++) {
await tryToProcess(items[i]);
}
}
async function processForEach(items) {
items.forEach(await tryToProcess);
}
async function tryToProcess(item) {
try {
await process(item);
} catch(error) {
await resolveAfter3Seconds(item);
}
}
// Asynchronous method
// Automatic failure for the sake of argument
function process(item) {
console.log(`Processing ${item}`);
return new Promise((resolve, reject) =>
setTimeout(() => reject(Error('process error message')), 1)
);
}
// Asynchrounous method
function resolveAfter3Seconds(x) {
return new Promise(resolve => setTimeout(() => {
console.log(`Resolved ${x} after 3 seconds.`);
resolve(x);
}, 3000));
}
Upvotes: 5
Views: 8748
Reputation: 42430
I'd like to understand why
forEach
doesn't play nicely withasync
/await
.
It's easier when we consider that async
is just syntactic sugar for a function returning a promise.
items.forEach(f) expects a function f
as argument, which it executes on each item one at at time before it returns. It ignores the return value of f
.
items.forEach(await tryToProcess)
is nonsense equivalent to Promise.resolve(tryToProcess).then(ttp => items.forEach(ttp))
and functionally no different from items.forEach(tryToProcess)
.
Now tryToProcess
returns a promise, but forEach
ignores the return value, as we've mentioned, so it ignores that promise. This is bad news, and can lead to unhandled rejection errors, since all promise chains should be returned or terminated with catch
to handle errors correctly.
This mistake is equivalent to forgetting await
. Unfortunately, there's no array.forEachAwait()
.
items.map(f) is a little better, since it creates an array out of the return values from f
, which in the case of tryToProcess
would give us an array of promises. E.g. we could do this:
await Promise.all(items.map(tryToProcess));
...but all tryToProcess
calls on each item would execute in parallel with each other.
Importantly, map
runs them in parallel. Promise.all
is just a means to wait for their completion.
I always use for of
instead of forEach
in async
functions:
for (const item of items) {
await tryToProcess(item);
}
...even when there's no await
in the loop, just in case I add one later, to avoid this foot-gun.
Upvotes: 15
Reputation: 370679
There is no way to use forEach
with await
like that - forEach
cannot run asynchronous iterations in serial, only in parallel (and even then, map
with Promise.all
would be better). Instead, if you want to use array methods, use reduce
and await
the resolution of the previous iteration's Promise:
let items = ['foo', 'bar', 'baz'];
processForEach(items);
async function processForLoop(items) {
for (let i = 0; i < items.length; i++) {
await tryToProcess(items[i]);
}
}
async function processForEach(items) {
await items.reduce(async(lastPromise, item) => {
await lastPromise;
await tryToProcess(item);
}, Promise.resolve());
}
async function tryToProcess(item) {
try {
await process(item);
} catch (error) {
await resolveAfter3Seconds(item);
}
}
// Asynchronous method
// Automatic failure for the sake of argument
function process(item) {
console.log(`Processing ${item}`);
return new Promise((resolve, reject) =>
setTimeout(() => reject(Error('process error message')), 1)
);
}
// Asynchrounous method
function resolveAfter3Seconds(x) {
return new Promise(resolve => setTimeout(() => {
console.log(`Resolved ${x} after 3 seconds.`);
resolve(x);
}, 3000));
}
Also note that if the only await
in a function is just before the function returns, you may as well just return the Promise itself, rather than have the function be async
.
Upvotes: 3