Async и await в JavaScript: последовательность, ошибки и параллельные задачи

Async и await в JavaScript: последовательность, ошибки и параллельные задачи
Содержание статьи

Функция с async всегда возвращает Promise. Оператор await приостанавливает выполнение этой функции до завершения ожидаемого значения, но не останавливает весь JavaScript. Последовательность определяется тем, когда вы вызываете функции: два вызова с await один за другим отличаются от запуска двух задач до общего ожидания.

В упражнении Mentoring.digital проверим это без сервера и запросов к чужому API. Таймер здесь нужен лишь для передачи управления, а не для измерения производительности.

Сначала проследите запуск и завершение

const events = [];
const pause = () => new Promise(resolve => setTimeout(resolve, 0));

async function task(name) {
  events.push(`start:${name}`);
  await pause();
  events.push(`end:${name}`);
  return name;
}

async function demo() {
  await task("A");
  await task("B");
  console.log(events.join(", "));

  events.length = 0;
  const result = await Promise.all([task("A"), task("B")]);
  console.log(events.slice(0, 2));
  console.log(result);
}

demo().catch(error => console.error(error));

Сохраните код в async-demo.js и запустите Node.js. Первый журнал содержит start:A, end:A, start:B, end:B: B вызывается только после завершения A. Во втором запуске первые две записи равны start:A и start:B: обе функции вызваны до ожидания общего результата.

Массив результата Promise.all равен ["A", "B"]. Его порядок соответствует порядку входных обещаний, а не скорости их завершения. При этом слова «параллельные задачи» здесь описывают перекрытие ожидания, а не выполнение обычного JavaScript-кода одновременно на двух процессорах.

Выберите порядок по зависимости данных

Если вторая операция требует id из результата первой, последовательность естественна: сначала получите пользователя, затем запросите его заказы. Если две независимые операции нужны для одного экрана, их можно запустить до общего await. Но запросы, которые изменяют одни и те же данные, нельзя считать независимыми только потому, что оба метода возвращают Promise.

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

Перехватите ошибку там, где её можно обработать

async function readValue() {
  throw new Error("Demo failure");
}

async function run() {
  try {
    const value = await readValue();
    return {ok: true, value};
  } catch (error) {
    return {ok: false, message: String(error)};
  }
}

Исключение из async-функции превращается в отклонение Promise. Чтобы try/catch внутри run поймал его, здесь используется await. Просто вызвать readValue() внутри try и выйти из блока недостаточно для обработки будущего отклонения.

Promise.all отклоняется при первой полученной ошибке одного из входов, но это не отменяет уже запущенные операции. Если задача требует собрать отдельные результаты всех операций, нужна другая политика обработки. Не подменяйте ошибку пустым успешным результатом, если вызывающий код должен отличать отсутствие данных от сбоя.

Почему await перед forEach не помогает

forEach не ждёт обещания, возвращённые callback. Поэтому запись await items.forEach(async item => ...) не ожидает окончания всех внутренних задач. Для последовательного выполнения используйте обычный for...of с await внутри. Для согласованного ожидания независимых операций создайте массив Promise и выберите подходящий метод ожидания.

Проверьте код на пустом массиве, ошибке первой операции и ошибке последней. Отдельно запишите, что должно случиться с остальными задачами при сбое. Асинхронный синтаксис не принимает это продуктовое решение за вас.

На курсе frontend с наставником порядок выполнения можно разобрать на собственном интерфейсе. Наставнику по frontend-разработке полезнее показать журнал событий и ожидаемую последовательность, чем только строку с await.

Похожие статьи по данной теме