Learn JavaScript

Promise.all: параллельные запросы

Часто нужно получить несколько независимых данных: например, список и настройки, профиль и уведомления, несколько JSON-файлов. Если делать await один за другим, получится последовательность и лишнее ожидание. В таких случаях помогает Promise.all.

1) Последовательно vs параллельно

Последовательно: медленнее

async function run() {
  const a = await fetch("/api/a").then((r) => r.json());
  const b = await fetch("/api/b").then((r) => r.json());
  const c = await fetch("/api/c").then((r) => r.json());

  console.log(a, b, c);
}

run();

Здесь запрос /api/b начнется только после /api/a, и так далее.

Параллельно: быстрее

async function run() {
  const pA = fetch("/api/a").then((r) => r.json());
  const pB = fetch("/api/b").then((r) => r.json());
  const pC = fetch("/api/c").then((r) => r.json());

  const [a, b, c] = await Promise.all([pA, pB, pC]);
  console.log(a, b, c);
}

run();

Запросы стартуют сразу, а Promise.all ждет их все.

2) Promise.all: что возвращает

Promise.all([...]) возвращает Promise, который:

  • выполняется успешно, когда все промисы успешны
  • возвращает массив результатов в том же порядке
  • отклоняется с первой попавшейся ошибкой (если какой-то промис упал)
Promise.all([Promise.resolve(1), Promise.resolve(2)]).then((values) => {
  console.log(values); // [1, 2]
});

3) Как правильно комбинировать с fetch: проверка response.ok

Частая ошибка - сразу делать .json(), не проверяя статус. Удобно вынести загрузку в функцию.

async function loadJSON(url) {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error("HTTP " + response.status);
  }

  return response.json();
}

async function run() {
  try {
    const [a, b] = await Promise.all([
      loadJSON("/api/a"),
      loadJSON("/api/b"),
    ]);

    console.log(a, b);
  } catch (e) {
    console.log(e.message);
  }
}

run();

4) Что происходит при ошибке

Если один из промисов упал, Promise.all сразу перейдет в catch.

async function run() {
  try {
    const values = await Promise.all([
      Promise.resolve("ok"),
      Promise.reject(new Error("fail")),
      Promise.resolve("later"),
    ]);

    console.log(values);
  } catch (e) {
    console.log(e.message); // "fail"
  }
}

run();

Важно: остальные промисы не “отменяются” автоматически, они могут продолжать выполняться, но результат уже не нужен.

5) Если нужно получить результаты даже при ошибках: Promise.allSettled

Promise.allSettled ждет все промисы и возвращает массив объектов со статусом.

async function run() {
  const results = await Promise.allSettled([
    Promise.resolve(1),
    Promise.reject(new Error("fail")),
    Promise.resolve(3),
  ]);

  console.log(results);
}

run();

Элементы будут вида:

  • { status: "fulfilled", value: ... }
  • { status: "rejected", reason: ... }

Пример фильтрации успешных результатов:

async function run() {
  const results = await Promise.allSettled([
    Promise.resolve(1),
    Promise.reject(new Error("fail")),
    Promise.resolve(3),
  ]);

  const okValues = results
    .filter((x) => x.status === "fulfilled")
    .map((x) => x.value);

  console.log(okValues); // [1, 3]
}

run();

6) Типовые ошибки

1) Случайно сделать последовательность, думая, что это параллельно

async function run() {
  const a = await loadJSON("/api/a");
  const b = await loadJSON("/api/b");
  const c = await loadJSON("/api/c");

  console.log(a, b, c);
}

Это последовательность. Для параллельности нужно сначала создать промисы, затем ждать вместе.

2) Передать в Promise.all не промисы

Promise.all ожидает массив промисов (или значений, которые можно привести к промисам). Если в массив случайно попали функции, они не запустятся сами.

const tasks = [
  () => fetch("/api/a"),
  () => fetch("/api/b"),
];

Promise.all(tasks).then((values) => {
  console.log(values);
});

Здесь tasks - это функции. Нужно вызвать их:

const tasks = [
  () => fetch("/api/a"),
  () => fetch("/api/b"),
];

Promise.all(tasks.map((fn) => fn())).then((responses) => {
  console.log(responses);
});

Что стоит запомнить

  • await один за другим - последовательность.
  • Promise.all нужен, когда задачи независимы и лучше запускать их параллельно.
  • Promise.all возвращает результаты в исходном порядке.
  • Одна ошибка отклоняет весь Promise.all.
  • Если нужны результаты всех запросов, даже с ошибками, подходит Promise.allSettled.