Promise.all と Promise.allSettled を使用した複数 API リクエストの処理
James Reed
Infrastructure Engineer · Leapcell

はじめに
現代のウェブ開発では、アプリケーションがさまざまなバックエンドサービスとやり取りすることは一般的であり、多くの場合、データの取得、リソースの更新、または複雑な操作の実行のために複数の API 呼び出しが必要となります。このような API リクエストのバッチを扱う場合、効率的で堅牢な処理が不可欠になります。2 つの強力な JavaScript Promise コンビネータである Promise.all
と Promise.allSettled
は、これらの並行操作を管理するための異なるアプローチを提供します。これらのニュアンスを理解することは、開発者が適切なツールを選択し、アプリケーションの安定性と最適なユーザーエクスペリエンスを確保するために重要です。この記事では、これらのメソッドの実際的な応用について掘り下げ、それらのコア機能、違いを検討し、効果的なバッチ API リクエスト処理のためにそれぞれいつ使用すべきかをガイドします。
コアコンセプトと実際的な応用
詳細に入る前に、JavaScript、特に Promises における非対称操作に関連するコアコンセプトについて共通の理解を確立しましょう。
Promises: Promise は、非同期操作の最終的な完了または失敗を表すオブジェクトです。次の 3 つの状態があります。
- Pending: 初期状態。fulfilled も rejected もされていません。
- Fulfilled: 操作が正常に完了したことを意味します。
- Rejected: 操作が失敗したことを意味します。
それでは、Promise.all
と Promise.allSettled
を見ていきましょう。
Promise.all の説明
Promise.all
は、Promises のイテラブル(例: Promises の配列)を受け取り、1 つの Promise を返します。返される Promise は、すべての入力 Promise が解決されると解決し、入力 Promise と同じ順序で解決された値の配列を返します。いずれかの入力 Promise が拒否された場合、最初に拒否された Promise の理由とともに、すぐに拒否されます。
原則: 「すべてか無か」。1 つのリクエストでも失敗した場合、バッチ操作全体が失敗と見なされます。これは、すべてのリクエストの成功が相互に依存している場合に理想的です。
ユースケース:
電子商取引アプリケーションを想像してみてください。製品ページを表示する前に、製品の詳細とユーザーレビューを同時に取得する必要があります。これらの API 呼び出しのいずれかが失敗した場合、不完全な製品ページを表示することは役立たないか、誤解を招く可能性があります。このシナリオでは、両方のデータが正常に取得されることを保証するために Promise.all
を使用したいでしょう。
実装例:
// API 呼び出しをシミュレート const fetchProductDetails = (productId) => { return new Promise((resolve, reject) => { setTimeout(() => { if (productId === 'p123') { resolve({ id: 'p123', name: 'Premium Widget', price: 29.99 }); } else { reject(new Error(`Product ${productId} not found`)); } }, 1000); }); }; const fetchUserReviews = (productId) => { return new Promise((resolve, reject) => { setTimeout(() => { if (productId === 'p123') { resolve([ { user: 'Alice', rating: 5, comment: 'Great product!' }, { user: 'Bob', rating: 4, comment: 'Good value.' } ]); } else if (productId === 'p456') { // レビューリクエストの失敗をシミュレート reject(new Error('Failed to fetch reviews for product p456')); } else { resolve([]); // 他の製品のレビューはありません } }, 800); }); }; const productId = 'p123'; // 例: 正常なシナリオ // const productId = 'p456'; // 例: 1 つのリクエストが失敗 Promise.all([ fetchProductDetails(productId), fetchUserReviews(productId) ]) .then(([productDetails, userReviews]) => { console.log("Promise.all Success:"); console.log("Product Details:", productDetails); console.log("User Reviews:", userReviews); // 両方のデータで製品ページをレンダリング }) .catch(error => { console.error("Promise.all Failed:", error.message); // ユーザーにエラーメッセージを表示するか、再試行 }); // 失敗するリクエストの例(テストするにはコメントを解除) /* const failingProductId = 'p456'; Promise.all([ fetchProductDetails(failingProductId), // これは成功するかもしれない fetchUserReviews(failingProductId) // これは拒否される ]) .then(([productDetails, userReviews]) => { console.log("Promise.all Success (ここに到達しないはずです):"), productDetails, userReviews); }) .catch(error => { console.error("Promise.all with failure:", error.message); // 出力: Failed to fetch reviews for product p456 // 1 つのリクエストが失敗したため、バッチ全体が失敗しました }); */
Promise.allSettled の説明
Promise.allSettled
も Promises のイテラブルを受け取り、1 つの Promise を返します。返される Promise は、すべての入力 Promise が fulfilled または rejected されたときに解決します。これは、各 Promise の結果を記述するオブジェクトの配列で解決されます。各オブジェクトには status
プロパティ('fulfilled'
または 'rejected'
)と、value
(fulfilled の場合)または reason
(rejected の場合)があります。
原則: 「個々の成功または失敗に関係なく、すべての結果を取得します。」これは、一部が失敗した場合でも、バッチ内のすべての操作の結果を知りたい場合に便利であり、成功した操作を続行できます。
ユースケース: ユーザーが画像を複数同時にアップロードしたいソーシャルメディアアプリケーションを考えてみてください。一部のアップロードはネットワークの問題や無効なファイルタイプにより成功するかもしれませんが、 others は失敗するかもしれません。アプリケーションは、1 つの画像が処理できなかっただけで、操作全体を失敗させるのではなく、各アップロードのステータス(例: 「画像 1 アップロード済み」、「画像 2 失敗」)をユーザーに通知する必要があります。
実装例:
// API 呼び出しをシミュレート const uploadImage = (fileName, artificialFailure = false) => { return new Promise((resolve, reject) => { setTimeout(() => { if (artificialFailure || fileName.includes('failed')) { reject(new Error(`Failed to upload ${fileName}`)); } else { resolve({ fileName, url: `https://example.com/images/${fileName}` }); } }, Math.random() * 1000 + 500); // 可変アップロード時間をシミュレート }); }; const imagesToUpload = [ 'photo1.jpg', 'avatar.png', 'corrupted_image.failed.gif', // これは失敗します 'document.pdf' ]; const uploadPromises = imagesToUpload.map(img => uploadImage(img)); Promise.allSettled(uploadPromises) .then(results => { console.log("Promise.allSettled Results:"); const successfulUploads = []; const failedUploads = []; results.forEach((result, index) => { if (result.status === 'fulfilled') { console.log(`Image ${imagesToUpload[index]} uploaded successfully:`, result.value.url); successfulUploads.push(result.value); } else { console.error(`Image ${imagesToUpload[index]} failed to upload:`, result.reason.message); failedUploads.push({ fileName: imagesToUpload[index], reason: result.reason.message }); } }); console.log("\nSummary:"); console.log("Successfully uploaded:", successfulUploads); console.log("Failed uploads:", failedUploads); // 各結果に基づいて UI を更新、例: 各アップロードの進捗を表示 }) .catch(error => { // この catch ブロックは通常、Promise.allSettled では到達しません // 入力イテラブル自体が無効であるか、同期エラーが発生した場合を除きます。 console.error("Unexpected error with Promise.allSettled:", error); });
Promise.all と Promise.allSettled の選択
この決定は、1 つ以上の API リクエストが失敗した場合に望ましい動作に大きく依存します。
-
Promise.all
を使用する場合:- すべての操作はクリティカルであり、相互に依存しています。1 つが失敗した場合、トランザクション全体はロールバックされるか、不完全と見なされるべきです。
- すべてのリクエストの集団的な成功のみが重要です。
- 迅速な失敗は許容可能または望ましい動作です。
-
Promise.allSettled
を使用する場合:- 個々の成功または失敗に関係なく、すべてのリクエストの結果を処理する必要があります。
- 操作は大部分独立しており、1 つの失敗がバッチ全体の失敗を必要としません。
- 成功した操作を処理しながら、個々の操作の成功と失敗に関する詳細なフィードバックをユーザーに提供したい場合。
- エラーを正常に処理しながら、成功した結果も処理したい場合。
結論
Promise.all
と Promise.allSettled
の両方とも、特にバッチ API リクエストのコンテキストで、複数の非同期操作をオーケストレーションするための貴重なツールです。Promise.all
は「すべてか無か」の契約を強制し、集団的な成功が最重要視される相互依存タスクに適しています。逆に、Promise.allSettled
はより回復力のあるアプローチを提供し、すべてのリクエストの最終結果を確認できるようにするため、部分的な成功が許容され、個々のステータスレポートが重要なシナリオに最適です。それらの異なる動作を理解し、適切に適用することにより、開発者はより堅牢で、耐障害性があり、ユーザーフレンドリーな JavaScript アプリケーションを構築できます。クリティカルで相互依存的な操作には Promise.all
を、個々の結果が重要な回復力のある処理には Promise.allSettled
を選択してください。