Node.js環境でのアプリケーション開発において、非同期処理の効率的な管理はシステムのレスポンス速度を左右する極めて重要な要素です。
特に複数の外部APIへのリクエストやデータベース操作、ファイルI/Oを扱う際、それらを一つずつ順番に実行していては、全体の処理時間が大幅に増大してしまいます。
Promise.allは、こうした複数の非同期タスクを並列に実行し、すべての完了を効率よく待機するための強力な仕組みを提供します。
本記事では、2026年時点のNode.js開発で標準的に用いられるPromise.allの基本的な使い方から、実務で必須となるエラーハンドリングのテクニック、さらには高負荷時を考慮したパフォーマンス最適化の実装パターンまでを詳しく提示します。
Node.jsにおけるPromise.allの役割とメリット
Node.jsはシングルスレッドで動作するイベントループモデルを採用していますが、I/O操作に関してはバックグラウンドで非同期に処理を進めることができます。
逐次処理(直列処理)を行う場合、前のタスクが完了するまで次のタスクを開始できないため、ネットワークの待機時間などが積み重なってしまいます。
Promise.allを利用すると、複数のPromiseを同時に開始し、それらがすべて解決されるのを待つことが可能になります。
これにより、実行時間の合計は「最も時間がかかるタスク」とほぼ同等になり、アプリケーションのレスポンスタイムを劇的に短縮できるというメリットがあります。
特にマイクロサービスアーキテクチャや、複数のデータソースから情報を集約する処理において、この並列実行の仕組みは欠かせません。
並列処理と直列処理の比較
並列処理と直列処理の違いを理解するために、それぞれの実行モデルを比較してみましょう。
直列処理では、タスクAの完了後にタスクBが始まり、その後にタスクCが始まるといった流れになります。
一方、Promise.allを用いた並列処理では、タスクA、B、Cを一斉に開始します。
| 比較項目 | 直列処理 (awaitを個別に使用) | 並列処理 (Promise.allを使用) |
|---|---|---|
| 実行順序 | 一つずつ順番に実行される | すべての処理がほぼ同時に開始される |
| 全体の所要時間 | 各タスクの処理時間の合計 | 最も時間がかかるタスクの処理時間 |
| CPU負荷 | 低い(分散されるため) | 一時的に高くなる可能性がある |
| 主な用途 | 前の結果に依存する処理 | お互いに依存しない独立した処理 |
Promise.allの基本的な使い方
Promise.allは、Promiseオブジェクトの配列(または反復可能オブジェクト)を引数に取ります。
すべてのPromiseが正常に完了(resolve)すると、各結果を格納した新しい配列を返します。
基本的にはasync/await構文と組み合わせて使用するのが、2026年現在のNode.js開発における標準的な記述スタイルです。
複数の非同期処理を並列実行する
まずは、複数のダミーAPIリクエストを模した処理を並列で実行する基本的なコードを見てみましょう。
// ダミーの非同期処理を定義
const fetchData = (id, delay) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`データ ${id} の取得が完了しました (${delay}ms)`);
resolve({ id, data: `結果 ${id}` });
}, delay);
});
};
async function main() {
console.log("処理を開始します...");
const start = Date.now();
try {
// 3つの処理を同時に開始する
const promises = [
fetchData(1, 1000),
fetchData(2, 2000),
fetchData(3, 1500)
];
// すべての完了を待機し、結果を配列で受け取る
const results = await Promise.all(promises);
const end = Date.now();
console.log("すべてのデータを受け取りました:", results);
console.log(`合計実行時間: ${end - start}ms`);
} catch (error) {
console.error("エラーが発生しました:", error);
}
}
main();
処理を開始します...
データ 1 の取得が完了しました (1000ms)
データ 3 の取得が完了しました (1500ms)
データ 2 の取得が完了しました (2000ms)
すべてのデータを受け取りました: [
{ id: 1, data: '結果 1' },
{ id: 2, data: '結果 2' },
{ id: 3, data: '結果 3' }
]
合計実行時間: 2005ms
実行結果から分かる通り、1000ms、2000ms、1500msの処理を同時に開始しているため、合計の待ち時間は最も長い2000ms強となっています。
また、Promise.allが返す結果の配列は、元のPromise配列と同じ順序で保持される点に注目してください。
これは、各タスクがどの順序で完了したかに関わらず、データの整合性を保つのに役立ちます。
Promise.allにおけるエラー制御(Fail-Fast)の特性
Promise.allを使用する際に最も注意すべき点は、その「Fail-Fast(即時失敗)」という挙動です。
渡されたPromiseのうち一つでも拒否(reject)されると、他のPromiseの完了を待たずに即座にエラーを返します。
この特性は、すべてのデータが揃っていないと後続の処理が継続できない場合には適しています。
一つがエラーになった場合の挙動
次の例では、2番目のタスクが失敗するように設定しています。
const taskSuccess = (name, time) => new Promise(resolve => setTimeout(() => resolve(`${name} 完了`), time));
const taskFailure = (name, time) => new Promise((_, reject) => setTimeout(() => reject(new Error(`${name} が失敗しました`)), time));
async function runTasks() {
try {
await Promise.all([
taskSuccess("タスク1", 1000),
taskFailure("タスク2", 500), // ここでエラーが発生
taskSuccess("タスク3", 2000)
]);
} catch (error) {
console.error("キャッチされたエラー:", error.message);
}
}
runTasks();
キャッチされたエラー: タスク2 が失敗しました
この場合、タスク3はバックグラウンドで動き続けますが、Promise.all自体の結果はタスク2が失敗した瞬間に確定してしまいます。
もし一つの失敗によって全体を中断させたくない場合は、別の手法を検討する必要があります。
個別エラーハンドリングの実装パターン
すべてのPromiseを完了させたい、あるいはエラーが発生したタスクだけを特定して処理したい場合は、各Promiseに対して個別に.catch()を付与する方法が有効です。
async function handleErrorsIndividually() {
const promises = [
taskSuccess("タスク1", 1000),
taskFailure("タスク2", 500).catch(err => ({ error: err.message })), // エラーを値として扱う
taskSuccess("タスク3", 2000)
];
const results = await Promise.all(promises);
console.log("実行結果:", results);
}
handleErrorsIndividually();
このように各Promiseでエラーを処理して「正常な値」として返却するようにすれば、Promise.allは途中で止まることなく最後まで実行されます。
2026年の推奨:Promise.allSettledの使い分け
Node.jsの比較的新しいバージョンでは、Promise.allSettledというメソッドが一般的に使用されています。
これはPromise.allとは異なり、すべてのPromiseが完了(解決または拒否)するまで必ず待ちます。
個別の成功・失敗の状態をオブジェクトとして配列に格納してくれるため、エラー制御がより簡単になります。
async function checkAllSettled() {
const results = await Promise.allSettled([
Promise.resolve("成功1"),
Promise.reject("失敗2"),
Promise.resolve("成功3")
]);
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`${index}: 成功 - 値: ${result.value}`);
} else {
console.log(`${index}: 失敗 - 理由: ${result.reason}`);
}
});
}
checkAllSettled();
0: 成功 - 値: 成功1
1: 失敗 - 理由: 失敗2
2: 成功 - 値: 成功3
「一部が失敗しても他の結果を利用したい」というケースでは、Promise.allSettledを選択するのが2026年のベストプラクティスの一つと言えます。
実務で役立つPromise.allの応用実装パターン
実際の開発現場では、単純な配列だけでなく、動的に生成されたリストに対してPromise.allを適用する場面が多くあります。
ここでは、代表的な2つのパターンについて解説します。
Array.mapとの組み合わせによる動的並列処理
データベースから取得したIDリストをもとに、複数の外部リソースを同時に取得するケースです。
async function fetchUserProfiles(userIds) {
// mapを使用してPromiseの配列を生成する
const userPromises = userIds.map(async (id) => {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) throw new Error(`User ${id} fetch failed`);
return response.json();
});
// 生成されたPromise配列をPromise.allに渡す
try {
const profiles = await Promise.all(userPromises);
return profiles;
} catch (error) {
console.error("プロフィール取得中にエラー:", error);
throw error;
}
}
Array.mapのコールバック関数をasyncにすることで、各要素の処理がPromiseを返すようになり、自然にPromise.allと連携できます。
大量のタスクを扱う際の同時実行数制限(セマフォ)
Promise.allは非常に便利ですが、数千、数万のタスクを一度に実行しようとすると、システムのリソース(メモリやファイル記述子)を使い果たしたり、接続先サーバーからレート制限を受けたりするリスクがあります。
このような場合には、同時実行数を一定数に抑えながら並列処理を行う「チャンク分割」や「ワーカープール」の考え方が必要です。
Node.jsのエコシステムでは p-limit などのライブラリが有名ですが、標準機能で実装する場合は以下のようなアプローチを取ります。
async function limitConcurrency(tasks, limit) {
const results = [];
const executing = new Set();
for (const task of tasks) {
const p = Promise.resolve().then(() => task());
results.push(p);
executing.add(p);
const clean = () => executing.delete(p);
p.then(clean).catch(clean);
if (executing.size >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
// 使用例
const tasks = Array.from({ length: 100 }, (_, i) => () => fetchData(i, Math.random() * 1000));
limitConcurrency(tasks, 10).then(res => console.log("全タスク完了"));
このように同時実行数を制御することで、大量のデータを扱うバッチ処理などでもNode.jsのプロセスを安定させることができます。
Promise.all使用時の注意点とパフォーマンスの最適化
並列処理を導入する際は、闇雲にすべての処理をPromise.allに入れるのではなく、依存関係を整理することが重要です。
依存関係のある処理を混ぜない
タスクBの実行にタスクAの結果が必要な場合、それらをPromise.allに含めることはできません。
論理的な順序を守りつつ、並列化可能な「独立した塊」を見つけることが設計の鍵となります。
メモリーリークの防止
非常に長い時間完了しないPromiseがPromise.allに含まれていると、その間すべての結果を保持するためにメモリーを消費し続けます。
タイムアウト処理(Promise.raceを用いた一定時間での拒否)を組み合わせて、リソースが永続的に確保されるのを防ぐ工夫も検討してください。
アンチパターンの回避
await Promise.all(array.map(async x => await foo(x))) のように、async/await を不必要に多用して可読性を下げてしまうケースがあります。
array.map(foo) のように直接Promiseを返す関数を渡すだけで十分な場合も多いため、コードを簡潔に保つよう意識しましょう。
まとめ
Node.jsにおけるPromise.allは、非同期処理の効率を最大限に引き出すための極めて重要なツールです。
複数の処理を並列化することで、全体の待機時間を削減し、ユーザー体験の向上やスループットの改善に直結します。
一方で、Fail-Fastの特性によるエラーハンドリングの難しさや、過剰な並列化によるリソース枯渇といった注意点も存在します。
2026年のモダンな開発においては、Promise.allだけでなく、安定性を重視したPromise.allSettledや、実行数を制限するテクニックを状況に応じて使い分けることが求められます。
本記事で紹介したパターンを参考に、スケーラブルで堅牢なNode.jsアプリケーションを構築してください。
