フォールトトレラントなPromise.allの設計と実装:堅牢な非同期処理の極意
現代のフロントエンド開発において、非同期処理の並列実行は避けて通れないタスクです。APIから複数のデータを取得し、それらを組み合わせてUIを構築する際、多くの方がPromise.allを選択するでしょう。しかし、Promise.allには「1つでも失敗すれば全体が失敗する(fail-fast)」という特性があり、これは多くの場合、ユーザー体験を損なう原因となります。本記事では、この課題を解決するための「フォールトトレラントなPromise.all」の設計手法について、深く掘り下げて解説します。
Promise.allの限界とfail-fastの弊害
Promise.allは、渡されたすべてのPromiseが解決(resolve)されるまで待機し、1つでも拒否(reject)されれば、その瞬間に全体が拒否されます。これは「すべてが揃わないと意味がない処理」には適していますが、マイクロサービスアーキテクチャや複雑なデータ連携を行うフロントエンドアプリケーションにおいては、致命的な弱点となります。
例えば、ダッシュボード画面で「ユーザープロフィール」「通知リスト」「最新ニュース」「広告枠」という4つのAPIを並列で叩くケースを想像してください。このうち「広告枠」のAPIだけがタイムアウトやエラーで失敗したとします。Promise.allを使用している場合、他の3つのデータが正常に取得できていたとしても、画面全体がエラー状態に陥ります。ユーザーからすれば「広告が見れないだけで、プロフィールすら表示されない」という不条理な体験を強いられることになります。これが、フォールトトレラントな並列処理が求められる最大の理由です。
Promise.allSettledによる解決策
ES2020で導入されたPromise.allSettledは、この問題に対する標準的な回答です。Promise.allSettledは、すべてのPromiseが「完了(解決または拒否)」するまで待機し、それぞれの結果をオブジェクトの配列として返します。
各要素は、状態を示すstatus(fulfilledまたはrejected)と、成功時のvalue、あるいは失敗時のreasonをプロパティとして持ちます。これにより、一部の失敗を許容しつつ、成功したデータのみを抽出してUIに適用することが可能になります。
サンプルコード:堅牢なデータ取得の実装
以下に、Promise.allSettledを活用した、実務でそのまま使えるフォールトトレラントなラッパー関数の実装例を示します。
/**
* フォールトトレラントな並列実行ヘルパー
* 特定の失敗を許容し、成功した結果のみを安全に抽出する
*/
async function fetchDashboardData() {
const endpoints = [
{ key: 'profile', promise: fetch('/api/profile') },
{ key: 'notifications', promise: fetch('/api/notifications') },
{ key: 'news', promise: fetch('/api/news') },
{ key: 'ads', promise: fetch('/api/ads') }
];
const results = await Promise.allSettled(endpoints.map(e => e.promise));
const data = {};
results.forEach((result, index) => {
const key = endpoints[index].key;
if (result.status === 'fulfilled') {
data[key] = result.value;
} else {
// エラーログの送信や、デフォルト値の適用
console.error(`Error fetching ${key}:`, result.reason);
data[key] = null; // またはデフォルト値
}
});
return data;
}
この実装では、個々のAPIの成功・失敗を個別にハンドリングしています。これにより、特定のサービスがダウンしていても、アプリケーション全体を停止させることなく、可能な限りの情報を提供し続ける「Graceful Degradation(段階的縮退)」を実現できます。
高度な技術:成功結果のフィルタリングと型安全性
TypeScript環境においては、Promise.allSettledの結果を適切に型付けすることが重要です。単に結果を配列として扱うだけでなく、成功したデータのみを抽出するユーティリティ関数を定義することで、コードの堅牢性が飛躍的に向上します。
type SettledResult<T> = PromiseSettledResult<T>;
function isFulfilled<T>(result: SettledResult<T>): result is PromiseFulfilledResult<T> {
return result.status === 'fulfilled';
}
// 使用例
const results = await Promise.allSettled([fetchDataA(), fetchDataB()]);
const successfulData = results
.filter(isFulfilled)
.map(r => r.value);
このパターンを利用すれば、成功した結果だけを安全に型推論された状態で取り扱うことができます。特に、大量の非同期処理を扱う場合、このフィルタリング処理は必須のテクニックとなります。
実務における注意点と設計判断
フォールトトレラントな設計を目指す上で、以下の3つの観点を常に意識する必要があります。
1. データの依存関係を評価する
すべてが独立しているわけではありません。例えば、「認証トークンを取得してからユーザー情報を取得する」ような依存関係がある場合、Promise.allSettledで並列化しようとすると、トークン取得の失敗がユーザー情報取得にも連鎖します。並列化すべき箇所と、直列で行うべき箇所を明確に区別することが、堅牢なアーキテクチャの第一歩です。
2. 「部分的な成功」をユーザーにどう伝えるか
データが一部欠落している場合、UI側でどう表示するかをあらかじめ決めておく必要があります。スケルトンローディングで待機させ続けるのか、エラー箇所だけ「現在情報を取得できません」と表示するのか。単に技術的にエラーを握りつぶすだけでなく、UXの設計とセットで考えることがスペシャリストの仕事です。
3. レートリミットとリソース管理
Promise.allSettledは、すべてのPromiseを同時に開始します。もしAPIの数が100個あれば、100個のネットワークリクエストが同時に発生します。ブラウザの同時接続数制限や、バックエンドサーバーへの負荷を考慮し、必要であれば「p-limit」のようなライブラリを使用して同時実行数を制御することも検討してください。
まとめ
Promise.allのfail-fastという特性は、シンプルさゆえに強力ですが、堅牢なフロントエンドアプリケーションを構築する際には「フォールトトレラント」なアプローチが不可欠です。Promise.allSettledを活用し、成功と失敗を明確に分離して処理する設計は、ユーザーに中断のない体験を提供するための最も効果的な手段の1つです。
技術選定において「何を使うか」だけでなく、「どこで失敗を許容し、どう回復させるか」という視点を持つことが、シニアエンジニアとしての質を決定づけます。本記事で紹介したパターンを自身のプロジェクトに取り入れ、より安定した、プロフェッショナルな非同期処理を実装してください。非同期処理の制御は、アプリケーションの品質そのものです。妥協なき設計を追求し続けましょう。

コメント