Parabolord
Parabolord

Reputation: 312

Await in catch block fails inside forEach

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

Answers (2)

jib
jib

Reputation: 42430

I'd like to understand why forEach doesn't play nicely with async/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.

As a rule...

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

CertainPerformance
CertainPerformance

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

Related Questions