Ошибка ожидания в блоке catch внутри forEach

У меня есть структура, подобная массиву, которая предоставляет асинхронные методы. Вызовы асинхронных методов содержат блоки try-catch, которые, в свою очередь, предоставляют больше асинхронных методов в случае обнаруженных ошибок. Я хотел бы понять, почему forEach плохо играет с 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.info(`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.info(`Resolved ${x} after 3 seconds.`);
        resolve(x);
    }, 3000));
}
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
2
0
3 973
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Невозможно использовать forEach с await таким образом - forEach не может запускать асинхронные итерации последовательно, только параллельно (и даже тогда map с Promise.all будет лучше). Вместо этого, если вы хотите использовать методы массива, используйте reduce и await с разрешением 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.info(`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.info(`Resolved ${x} after 3 seconds.`);
    resolve(x);
  }, 3000));
}

Также обратите внимание, что если единственный await в функции находится непосредственно перед возвратом функции, вы также можете просто вернуть само обещание, а не использовать функцию async.

(и даже тогда map с Promise.all будет лучше) ... В моем случае оба асинхронных метода включают ввод-вывод, поэтому, если я не ошибаюсь, использование Promise.all было бы плохой идеей. Спасибо, мне очень помогли.

Parabolord 30.05.2018 02:55

Это только в том случае, если вы хотите запускать итерации параллельно, но вы дали понять, что хотите запускать их последовательно, таким образом, метод reduce.

CertainPerformance 30.05.2018 02:57

На самом деле нет причин предпочесть шаблон reducefor of внутри функций async.

jib 30.05.2018 04:31

I'd like to understand why forEach doesn't play nicely with async/await.

Это проще, если учесть, что async - это просто синтаксический сахар для функции, возвращающей обещание.

items.для каждого (f) ожидает функцию f в качестве аргумента, которую она выполняет для каждого элемента по очереди перед возвратом. Он игнорирует возвращаемое значение f.

items.forEach(await tryToProcess) - это ерунда, эквивалентная Promise.resolve(tryToProcess).then(ttp => items.forEach(ttp))

и функционально ничем не отличается от items.forEach(tryToProcess).

Теперь tryToProcess возвращает обещание, но forEach игнорирует возвращаемое значение, как мы уже упоминали, поэтому игнорирует это обещание. Это плохая новость и может привести к необработанным ошибкам отклонения, поскольку все цепочки обещаний должны быть возвращены или завершены с помощью catch для правильной обработки ошибок.

Эта ошибка эквивалентна забвению await. К сожалению, array.forEachAwait() нет.

items.карта (f) немного лучше, поскольку он создает массив из возвращаемых значений из f, который в случае tryToProcess даст нам массив обещаний. Например. мы могли бы сделать это:

await Promise.all(items.map(tryToProcess));

... но все вызовы tryToProcess для каждого элемента будут выполнять в параллели друг с другом.

Важно отметить, что map запускает их параллельно. Promise.all - это просто средство дождаться их завершения.

Как правило...

Я всегда использую for of вместо forEach в функциях async:

for (item of items) {
  await tryToProcess(item);
}

... даже когда в петле нет await, на всякий случай я добавлю еще один позже, чтобы избежать этого ножного пистолета.

Другие вопросы по теме