Перейти к содержимому
Теория · Асинхронность · урок 10 из 12

Несколько промисов сразу

Представьте, что повару нужно испечь два пирога. Можно поставить в духовку первый, дождаться его и только потом поставить второй. А можно поставить оба сразу: тогда ждать придётся один раз, а не два.

Два await подряд работают по первому способу. Второй таймер ставится только после того, как сработал первый:

JavaScript
function wait(ms, value) {
    return new Promise((resolve) => setTimeout(() => {
        console.log("ready", value);
        resolve(value);
    }, ms));
}

async function main() {
    await wait(100, "a");
    await wait(100, "b");
}

main();
// ready a
// ready b

Разберём, что происходит:

  1. main вызывает wait(100, "a") и ждёт на первом await.
  2. Через 100 миллисекунд таймер печатает ready a, и промис выполняется.
  3. Только теперь начинается вызов wait(100, "b"), и ещё через 100 миллисекунд печатается ready b.

Всего проходит около 200 миллисекунд. Когда второе ожидание не зависит от первого, такой порядок только теряет время. Оба промиса можно запустить сразу и ждать их вместе.

Promise.all

Promise.all принимает массив промисов и возвращает один промис. Этот промис выполняется, когда выполнены все промисы массива, и его значение — массив их значений. Порядок значений совпадает с порядком в исходном массиве, а не с тем, кто закончил первым:

JavaScript
async function both() {
    const [a, b] = await Promise.all([wait(200, "slow"), wait(100, "fast")]);
    console.log(a, b);
}

both();
// ready fast
// ready slow
// slow fast

Давайте разберём по шагам:

  1. Оба вызова wait выполняются сразу, ещё до Promise.all, и оба таймера начинают отсчёт.
  2. Через 100 миллисекунд срабатывает таймер fast и печатает ready fast.
  3. Через 200 миллисекунд срабатывает таймер slow и печатает ready slow.
  4. Теперь выполнены оба промиса, и await даёт массив ["slow", "fast"].

В результате "slow" стоит первым, как в исходном массиве, хотя готов он позже. Поэтому деструктуризация раскладывает значения по именам без путаницы. Так повар подаёт блюда в порядке заказа, а не в порядке готовности.

Сам Promise.all ничего не запускает: таймеры начинают отсчёт в момент вызова wait, ещё до него. Promise.all только ждёт промисы, которые уже созданы. Если массив пустой, промис из Promise.all сразу выполняется со значением [].

Это частый вопрос на собеседовании: как дождаться нескольких независимых промисов быстрее, чем по очереди? Ответ: создать их все сразу и ждать через Promise.all.

Первый отказ

Если хотя бы один промис отклонён, промис из Promise.all сразу отклоняется с его причиной и остальных не ждёт:

JavaScript
async function failFast() {
    try {
        await Promise.all([wait(100, "a"), Promise.reject(new Error("b failed"))]);
        console.log("all done");
    } catch (e) {
        console.log(e.message);
    }
}

failFast();
// b failed
// ready a

Отказ Promise.all бросается на строке с await, как отказ одного промиса, поэтому его ловит try…catch. Строка all done не печатается, а ошибка появляется раньше, чем срабатывает таймер a.

Здесь аналогия с поваром перестаёт работать. Повар, у которого сгорел один пирог, может вынуть из духовки и второй. А Promise.all ничего не отменяет: таймер a всё равно печатает ready a, но его значение уже никто не получит.

Асинхронный код вы отрабатываете и в задачах курса. Повтор вызова через await и try…catch вы уже написали в задаче перед этим уроком. После теста к нему вас ждёт задача на промис с ограничением времени.

Частая ошибка

Промисы передали отдельными аргументами, а не одним массивом:

JavaScript
Promise.all(wait(100, "a"), wait(100, "b")); // ✗
TEXT
TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))

Promise.all берёт только первый аргумент и пытается обойти его, как массив. Первый аргумент — промис, а не массив, и перебрать его по элементам нельзя. Слово object в сообщении и означает этот промис. Хвост в скобках — внутреннее имя способа перебора, его можно не читать. Оберните промисы в квадратные скобки: Promise.all([wait(100, "a"), wait(100, "b")]).

Главное

  • Два await подряд ждут по очереди, а независимые промисы можно создать сразу и ждать через Promise.all.
  • Значения из Promise.all идут в порядке исходного массива, а не в порядке готовности.
  • Первый же отказ сразу отклоняет промис из Promise.all, но остальная работа не отменяется.
  • Promise.all принимает один массив, а не промисы отдельными аргументами.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом