Promise.raceをフォールバックに使うのはやめよう:Promise.anyとAggregateErrorのすすめ

Table of Contents
Promise.any
Promise.any
AggregateError
AggregateError
Promise.all
Promise.all
Promise.allSettled
Promise.allSettled
Promise.race
Promise.race
Promise.anyとAggregateError:必要だと知らなかったパターン
私は数ヶ月間、Promise.anyを無視していました。なぜなら、それは単にPromise.allの反対だと思っていたからです。興味深いけれど、ニッチなものだと。しかし、それは間違いでした。これは、以前はぎこちない回避策が必要だった現実世界の問題群に対する、最もクリーンな解決策です。
コンビネーターの状況
JavaScriptには4つのPromiseコンビネーターがあります。これらはファミリーとして機能します。個別に学ぶよりも、まとめて理解する方が早いです。
| コンビネーター | 解決する条件 | 拒否する条件 |
|---|---|---|
Promise.all | すべてのPromiseが解決する | いずれかのPromiseが拒否する |
Promise.allSettled | すべてのPromiseが解決する(常に) | 決して拒否しない — 常に解決する |
Promise.race | いずれかのPromiseが解決する | いずれかのPromiseが拒否する |
Promise.any | いずれかのPromiseが解決する | すべてのPromiseが拒否する |
Promise.anyは空の象限に位置します。最初の成功で解決し、失敗をショートサーキットするのではなく、集約します。それがこの機能の全てです。
覚え方
any + settledはそれぞれrace + allのように振る舞います。any = 最初の勝利、settled = 全員を待つ。これら4つの名前を覚える代わりに、この2つの軸を組み合わせて考えることができます。
それが置き換える欠陥のあるアプローチ
「フォールバック」の動作が必要なときに、私が以前書いていたコードは次のとおりです。
| 古いパターン | 実際に必要だったもの | |
|---|---|---|
| Aを試行し、失敗したらBを試行し、失敗したらCを試行する | 最初の成功で解決 — 逐次的な待機なし、帯域幅の無駄なし | 最初の成功にはPromise.anyを使用する |
| どのフェッチが解決したかを確認し、残りを無視する | AggregateErrorは、その詳細を無料で正確に提供する | どのPromiseが最初に解決したかを手動で検査する必要がある |
| タイムアウトまたはフォールバックのための`Promise.race` | `Promise.any` — 最初の成功、正直な失敗報告 | Raceは成功ではなく解決で勝利する — 失敗を静かに飲み込む |
古いアプローチでは、最速のサービスを呼び出し、それが良好であればその結果を使用し、そうでなければフォールバックしていました。Promise.raceは魅力的ですが、競合(race)で静かに失敗します。勝者が成功したのか、単に最初に反応しただけなのかがわかりません。Promise.anyはこれを直接解決します。
基本的な使い方
最小限の例
const urls = [
"https://api.primary.com/data",
"https://api.backup-1.com/data",
"https://api.backup-2.com/data",
];
try {
const response = await Promise.any(
urls.map((url) => fetch(url).then((r) => r.json()))
);
console.log(
"Got data from first successful fetch:",
response
);
} catch (aggregateError) {
// All three failed — see exactly why
console.log(
"All endpoints failed:",
aggregateError.errors
);
aggregateError.errors.forEach((err, i) => {
console.log(`Source ${i}:`, err.message);
});
}
重要な詳細:Promise.anyが拒否された場合、エラーは通常のErrorではなく、AggregateErrorです。これは、入力イテラブルと同じ順序で、失敗した入力ごとに1つのエントリを持つ.errors配列を保持します。
明らかではないユースケース
1. フォールバックを伴う並列機能フラグフェッチ。 3つのフラグプロバイダーを同時に呼び出します。最初に応答したプロバイダーの結果が必要で、すべてのプロバイダーがダウンした場合にのみ失敗を気にします。
2. raceセマンティクスなしのタイムアウトラッパー。 遅い呼び出しをタイムアウトでラップします。
async function fetchWithTimeout(url, ms) {
return Promise.any([
fetch(url),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error("Timeout")),
ms
)
),
]);
}
これはPromise.raceよりもクリーンです。なぜなら、タイムアウトは拒否のみを消費し、解決は消費しないからです。
3. プログレッシブ画像読み込み。 ブラウザがサポートする最初の形式で解決します。
const sources = [
fetchImage("webp"),
fetchImage("avif"),
fetchImage("png"), // fallback
];
const image = await Promise.any(sources); // Use whichever format loaded first
4. 堅牢な認証トークン更新。 2つのプロバイダーから同時に更新し、成功した方を使用します。
const tokens = await Promise.any([
refreshGitHubToken(),
refreshGitLabToken(),
]); // Use tokens regardless of which provider responded
AggregateErrorの詳細
AggregateErrorオブジェクトは、単なる拒否以上のものを明らかにします。
try {
await Promise.any(failingPromises);
} catch (err) {
// err is an AggregateError
console.log(err.name); // "AggregateError"
console.log(err.message); // "All promises were rejected"
console.log(err.errors); // [TypeError, ReferenceError, RangeError]
...
}
err.errorsは個々のエラーオブジェクトを保持するため、ソースごとにロジックを分岐させることができます。これにより、Promise.anyは「最初の成功」のセマンティクスだけでなく、ロギングやエラー報告に真に役立つものとなります。
並列化のレバー
I/Oを並列で実行することでレイテンシを平坦化することは、JavaScriptの最適化の最も古いトリックです。Promise.allは「すべて成功する必要がある」ケースをカバーしました。Promise.anyは「少なくとも1つが成功する」ケースをカバーします。これはほとんどの現実世界の要求に当てはまります。
比較:
- 失敗時に再試行する単一のAPI呼び出しは逐次的なオーバーヘッドです。各試行でタイムアウトを待つことになります。
Promise.anyを使用して2つのサービスに並列で発行される同じリクエストは、両方のうち最速のものによって時間が制限されます。遅い失敗コストは、より速いタイムアウトに制限されます。
この非対称性は、個々のタイムアウトが長いモバイルや高レイテンシ接続で最も重要になります。
互換性と注意点
- Node.js 15+、Deno 1.0+、またはすべてのモダンブラウザ(Chrome 85+、Firefox 79+、Safari 14+)が必要です。Node 14では利用できません。
- すべての入力が拒否された場合、結果の
AggregateError.messageは常にリテラル文字列"All promises were rejected"です。デバッグのためにこれを解析せず、代わりに.errorsを使用してください。 - Promise以外のイテラブルエントリは、他のPromiseメソッドとまったく同じように強制されます(ES2020以降の一貫した動作)。
Promise.anyは解決した場合にのみ.thenの呼び出しを停止するため、別の入力がすでに解決した後でも、拒否する入力は拒否されます。追加の拒否は、キャッチされない限り、未処理の拒否警告を生成します。- 空のイテラブルはすぐに拒否されます。
Promise.any([])はAggregateErrorで拒否され、その.errors配列は空です。これは、Promise.all([])が[]で解決するのとは異なります。
クイックリファレンス
// Basic: first success wins
const winner = await Promise.any([
fetch(urlA),
fetch(urlB),
fetch(urlC),
]);
// Fallback: value from whichever call resolved first
// All fail: catch AggregateError — inspect .errors
// Timeout pattern
async function withTimeout(promise, ms) {
return Promise.any([
promise,
new Promise((_, reject) =>
setTimeout(
() => reject(
new Error(`Timed out after ${ms}ms`)
),
ms
)
),
]);
}
// Typed (TypeScript)
function firstSuccessful<T>(
promises: Array<Promise<T>>
): Promise<T> {
return Promise.any(promises);
}
こちらもどうぞ
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

PlaywrightによるE2Eテスト習得2026年版
Playwrightのauto-waiting、browser context isolation、network interception、auth storage、CI parallelizationを活用し、E2Eテストを習得するためのガイドです。
Read more
エンタープライズアプリケーションのためのTypeScript高度パターン
branded type、条件付き応答型、テンプレートリテラルルーティング、satisfies演算子など、エンタープライズ向けTypeScriptの高度なパターンを習得しましょう。
Read more
ReactのState管理2026: Reduxの先へ
React19のactions、TanStack Queryのサーバーstate、Zustand、Jotai、Signalsを比較し、2026年のReactにおけるstate管理を包括的に解説します。
Read more