はじめに
JavaScriptの非同期処理について学ぶため、今回は複数のPromiseを扱う以下の4つのメソッドを実際に動かしてみます。
Promise.all()Promise.allSettled()Promise.race()Promise.any()
いずれも複数のPromiseをまとめて扱うためのメソッドですが、
「いつ処理全体がsettledになるのか」「rejectをどう扱うのか」がそれぞれ異なります。
今回は書籍検索を題材にし、検索処理に意図的な遅延を設定して、それぞれの挙動を確認します。
実験に使用するデータ
const books = {
"9784480096432": {
isbn: "9784480096432",
title: "物と心",
author: "大森 荘蔵"
},
"9784782800157": {
isbn: "9784782800157",
title: "流れとよどみ",
author: "大森 荘蔵"
},
"9784065249444": {
isbn: "9784065249444",
title: "新視覚新論",
author: "大森 荘蔵"
},
"9784782800225": {
isbn: "9784782800225",
title: "名指しと必然性",
author: "Saul Kripke"
},
"9784582770124": {
isbn: "9784582770124",
title: "指示と存在",
author: "Saul Kripke"
}
};
const authors = {
"大森 荘蔵": {
name: "大森 荘蔵",
majorWorks: ["9784480096432", "9784782800157", "9784065249444"]
},
"Saul Kripke": {
name: "Saul Kripke",
majorWorks: ["9784782800225", "9784582770124"]
}
};
共通で使用する関数
/**
* ISBNから書籍を検索する。
*
* @param {string} isbn ISBN
* @param {number} delay 遅延時間(ミリ秒)
* @returns {Promise<object>}
*/
function findBook(isbn, delay) {
return new Promise((resolve, reject) => {
console.log(
`書籍検索開始: ${isbn}(${delay}ms)`
);
setTimeout(() => {
const book = books[isbn];
if (book === undefined) {
reject(
new Error(`書籍が見つかりません: ${isbn}`)
);
return;
}
console.log(
`書籍検索完了: ${book.title}(${delay}ms)`
);
resolve(book);
}, delay);
});
}
存在しないISBNを指定するとPromiseをrejectします。
今回は実際の通信処理を行うのではなく、setTimeout()によって時間のかかる非同期処理を再現しています。
Promise.all()
サンプルコード
async function processWithPromiseAll() {
console.log("=== Promise.all() 開始 ===");
try {
const results = await Promise.all([
findBook("9784480096432", 3000),
findBook("9784782800157", 1000),
findBook("9784065249444", 2000)
]);
console.log("すべての検索が完了しました。");
results.forEach((book) => {
console.log(
`- ${book.title} / ${book.author}`
);
});
} catch (error) {
console.error("エラー:", error.message);
}
}
processWithPromiseAll();
概要・仕様
Promise.all()は、渡されたPromiseがすべてfulfilledになるまで待ちます。
すべて成功すると、それぞれのPromiseの結果を配列として取得できます。
1つでもrejectすると、Promise.all()もrejectします。
また、結果の配列はPromiseが完了した順番ではなく、Promise.all()に渡した順番になります。
今回の例では、検索処理は1000ms、2000ms、3000msの順に完了しますが、結果の配列は指定した順番になります。
実行結果
=== Promise.all() 開始 ===
書籍検索開始: 9784480096432(3000ms)
書籍検索開始: 9784782800157(1000ms)
書籍検索開始: 9784065249444(2000ms)
書籍検索完了: 流れとよどみ(1000ms)
書籍検索完了: 新視覚新論(2000ms)
書籍検索完了: 物と心(3000ms)
すべての検索が完了しました。
- 物と心 / 大森 荘蔵
- 流れとよどみ / 大森 荘蔵
- 新視覚新論 / 大森 荘蔵
Promise.allSettled()
サンプルコード
async function processWithPromiseAllSettled() {
console.log("=== Promise.allSettled() 開始 ===");
const results = await Promise.allSettled([
findBook("9784480096432", 3000),
findBook("存在しないISBN", 1000),
findBook("9784065249444", 2000)
]);
console.log("すべての検索が終了しました。");
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(
`${index + 1}: 成功 - ${result.value.title}`
);
} else {
console.log(
`${index + 1}: 失敗 - ${result.reason.message}`
);
}
});
}
processWithPromiseAllSettled();
概要・仕様
Promise.allSettled()は、複数のPromiseがすべてsettledになるまで待ちます。
Promise.all()と異なり、途中でrejectしても処理全体がrejectされることはありません。
各Promiseについて、成功・失敗の両方を結果として取得できます。
成功した場合は、
{
status: "fulfilled",
value: ...
}
失敗した場合は、
{
status: "rejected",
reason: ...
}
という結果になります。
今回の例では、存在しないISBNを1つ含めているため、成功した検索と失敗した検索の両方を確認できます。
結果の配列は、Promiseがsettledになった順番ではなく、
Promise.allSettled()に渡した順番になります。
実行結果
=== Promise.allSettled() 開始 ===
書籍検索開始: 9784480096432(3000ms)
書籍検索開始: 存在しないISBN(1000ms)
書籍検索開始: 9784065249444(2000ms)
書籍検索完了: 新視覚新論(2000ms)
書籍検索完了: 物と心(3000ms)
すべての検索が終了しました。
1: 成功 - 物と心
2: 失敗 - 書籍が見つかりません: 存在しないISBN
3: 成功 - 新視覚新論
Promise.race()
サンプルコード
async function processWithPromiseRace() {
console.log("=== Promise.race() 開始 ===");
try {
const result = await Promise.race([
findBook("9784480096432", 3000),
findBook("9784782800157", 1000),
findBook("9784065249444", 2000)
]);
console.log("最初に結果が確定した検索:");
console.log(
`- ${result.title} / ${result.author}`
);
} catch (error) {
console.error("エラー:", error.message);
}
}
processWithPromiseRace();
概要・仕様
Promise.race()は、複数のPromiseのうち、最初にsettledになったPromiseの結果を採用します。
settledとは、Promiseが以下のいずれかになった状態です。
- fulfilled
- rejected
つまり、「最初に成功したPromise」ではありません。
最初にfulfilledになった場合はfulfilledになり、最初にrejectedになった場合はrejectedになります。
今回の例では、1000msの検索が最初に完了するため、その結果が返されます。
なお、Promise.race()で結果が決まったからといって、他のPromiseが自動的にキャンセルされるわけではありません。
実行結果
=== Promise.race() 開始 ===
書籍検索開始: 9784480096432(3000ms)
書籍検索開始: 9784782800157(1000ms)
書籍検索開始: 9784065249444(2000ms)
書籍検索完了: 流れとよどみ(1000ms)
最初に結果が確定した検索:
- 流れとよどみ / 大森 荘蔵
書籍検索完了: 新視覚新論(2000ms)
書籍検索完了: 物と心(3000ms)
Promise.any()
サンプルコード
async function processWithPromiseAny() {
console.log("=== Promise.any() 開始 ===");
try {
const result = await Promise.any([
findBook("9784480096432", 3000),
findBook("9784782800157", 1000),
findBook("9784065249444", 2000)
]);
console.log("最初に成功した検索:");
console.log(
`- ${result.title} / ${result.author}`
);
} catch (error) {
console.error("エラー:", error);
}
}
processWithPromiseAny();
概要・仕様
Promise.any()は、複数のPromiseのうち、最初にfulfilledになったPromiseの結果を採用します。
Promise.race()と似ていますが、重要な違いがあります。
Promise.race()は、最初にsettledになったPromiseを採用します。
一方、Promise.any()は、最初にfulfilledになったPromiseを採用します。
そのため、最初にrejectしたPromiseがあっても、その時点では処理を終了しません。
すべてのPromiseがrejectした場合には、Promise.any()もrejectします。
この場合、rejectの理由にはAggregateErrorが使用され、
各Promiseがrejectした理由はerror.errorsで取得できます。
実行結果
=== Promise.any() 開始 ===
書籍検索開始: 9784480096432(3000ms)
書籍検索開始: 9784782800157(1000ms)
書籍検索開始: 9784065249444(2000ms)
書籍検索完了: 流れとよどみ(1000ms)
最初に成功した検索:
- 流れとよどみ / 大森 荘蔵
書籍検索完了: 新視覚新論(2000ms)
書籍検索完了: 物と心(3000ms)
4つのメソッドの比較
| メソッド | 待つ条件 | rejectの扱い |
|---|---|---|
Promise.all() |
すべてfulfilled | 1つでもrejectするとreject |
Promise.allSettled() |
すべてsettled | 成功・失敗の両方を結果として取得 |
Promise.race() |
最初にsettled | 最初がrejectならreject |
Promise.any() |
最初にfulfilled | rejectは無視し、すべてrejectしたらreject |
特にPromise.race()とPromise.any()は混同しやすいので注意が必要です。
Promise.race()
↓
最初にsettledしたPromise
↓
fulfilledでもrejectedでもよい
Promise.any()
↓
最初にfulfilledしたPromise
↓
rejectedは無視
まとめ
今回は、複数のPromiseを扱う4つのメソッドを実際に動かしました。
-
Promise.all()は、すべてのPromiseがfulfilledになるまで待つ -
Promise.allSettled()は、すべてのPromiseがsettledになるまで待ち、成功・失敗の両方を取得する -
Promise.race()は、最初にsettledになったPromiseを採用する -
Promise.any()は、最初にfulfilledになったPromiseを採用する
4つは名前が似ていますが、「何を待つのか」「rejectをどう扱うのか」に注目すると、それぞれの違いを整理できます。