Часто нужно получить несколько независимых данных: например, список и настройки, профиль и уведомления, несколько 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.