ジャンルから探す

Web Design English

JavaScriptのアップデート:Promise.any() の標準化

Promise.any()は、複数の非同期処理のうち「最初に成功したもの」だけを受け取るメソッドです。全部失敗したときはAggregateErrorを投げます。ES2021で標準化され、現在は全モダンブラウザと Node.js 15 以降で使えます。

使いどころははっきりしていて、ミラーサーバーやCDNのフォールバックです。逆に、true/false を返す判定処理には使えません(後述しますが、これは非常によくある誤用です)。この記事では、正しい使い方と、all / race / allSettled との使い分けを整理します。

スポンサーリンク

Promise.any() の基本動作

最初に fulfilled になった Promise の値を返し、それ以外は無視します。 失敗(rejected)は、全部が失敗するまで結果に影響しません。

const p1 = Promise.reject(new Error('エラー1'));
const p2 = new Promise((resolve) => setTimeout(resolve, 100, '成功2'));
const p3 = new Promise((resolve) => setTimeout(resolve, 500, '成功3'));

const result = await Promise.any([p1, p2, p3]);
console.log(result); // '成功2'

p1は即座に失敗しますが、Promise.any()は待ち続け、100ms後に成功したp2の値を返します。p3の結果は捨てられます(処理自体は止まりません。これは後述します)。

全て失敗したとき:AggregateError

全ての Promise が rejected になると、AggregateErrorが投げられます。errorsプロパティに、元の失敗理由が配列で入っているのが特徴です。

try {
  await Promise.any([
    Promise.reject(new Error('エラー1')),
    Promise.reject(new Error('エラー2')),
    Promise.reject(new Error('エラー3')),
  ]);
} catch (err) {
  if (err instanceof AggregateError) {
    console.error('すべて失敗しました');
    err.errors.forEach((e) => console.error(' -', e.message));
    // - エラー1
    // - エラー2
    // - エラー3
  }
}

空の配列を渡した場合も即座にAggregateErrorで reject されます。 動的に配列を組み立てるコードでは、この分岐を忘れると原因不明のエラーになります。

const sources = getSources(); // 条件によっては空になる

if (sources.length === 0) {
  throw new Error('取得先が1つもありません');
}

const data = await Promise.any(sources.map(fetchFrom));

スポンサーリンク

正しい使いどころ:フォールバック

同じデータを複数の場所から取れるときが本来の用途です。どこか1つでも応答すればよく、しかも一番速いものを使いたい、という状況にぴったり合います。

async function fetchJson(url, signal) {
  const res = await fetch(url, { signal });
  if (!res.ok) throw new Error(`HTTP ${res.status} (${url})`);
  return res.json();
}

const MIRRORS = [
  'https://cdn1.example.com/data.json',
  'https://cdn2.example.com/data.json',
  'https://origin.example.com/data.json',
];

try {
  const data = await Promise.any(MIRRORS.map((u) => fetchJson(u)));
  render(data);
} catch (err) {
  // AggregateError。全ミラーがダウンしている
  console.error('全ての取得先が失敗:', err.errors);
  showMessage('データを取得できませんでした');
}

ここで「HTTPエラーを例外にする」処理が重要です。fetch()は404や500でも Promise を fulfilled にするため、res.okを確認しないと「404のレスポンスが最速だったのでそれを採用する」という結果になります。

やってはいけない使い方:判定処理

ネット上のサンプルでよく見かける、次のようなコードは意図通りに動きません。

// ❌ 動かない例
const validateEmail = (v) => Promise.resolve(/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v));
const validatePhone = (v) => Promise.resolve(/^\d{10}$/.test(v));

Promise.any([
  validateEmail('これはメールではない'),
  validatePhone('あいうえお'),
]).then((r) => console.log('どれかに合格:', r));

このコードは「どれかに合格: false」と出力します。理由は単純で、Promise.resolve(false)は「falseという値で成功したPromise」だからです。Promise.any()が見ているのは値の中身ではなく、fulfilled か rejected かだけです。

判定に使いたいなら、失敗を reject で表現する必要があります。

// ✅ 失敗を reject にする
const validateEmail = async (v) => {
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) throw new Error('メール形式ではありません');
  return { type: 'email', value: v };
};

const validatePhone = async (v) => {
  if (!/^\d{10}$/.test(v)) throw new Error('電話番号形式ではありません');
  return { type: 'phone', value: v };
};

try {
  const matched = await Promise.any([validateEmail(input), validatePhone(input)]);
  console.log('形式:', matched.type);
} catch (err) {
  console.log('どの形式にも当てはまりません');
}

そもそも同期的に判定できる処理を Promise でくるむ意味はありません。上の例は書き方の説明用であって、実務ではArray.prototype.some()で十分です。Promise.any()を使う価値があるのは、本当に時間のかかる処理が並列に走る場面だけです。

スポンサーリンク

負けた処理は止まらない

見落としやすい点として、Promise.any()が決着しても、残りの処理は動き続けます。Promiseには中断の仕組みが無いためです。

通信であれば、AbortControllerで明示的に止める必要があります。

const controller = new AbortController();

try {
  const data = await Promise.any(
    MIRRORS.map((u) => fetchJson(u, controller.signal))
  );
  render(data);
} finally {
  // 勝者が決まったら残りの通信を打ち切る
  controller.abort();
}

finallyに置くのがポイントです。成功時も失敗時も、余計なリクエストを残しません。これを書かないと、ミラーの数だけ通信が最後まで走り続け、通信量と回線を無駄に消費します。

all / allSettled / race / any の使い分け

メソッド 決着する条件 返る値 使う場面
Promise.all() 全部成功/1つでも失敗 値の配列 全部そろわないと画面が作れない
Promise.allSettled() 全部決着 状態つきの配列 一部失敗しても表示を続けたい
Promise.race() 最初に決着した1つ(成功・失敗問わず) その値/理由 タイムアウトとの競争
Promise.any() 最初に成功した1つ その値 フォールバック、ミラー

race()とany()の違いが最も間違えやすいところです。

const fast = Promise.reject(new Error('速いが失敗'));
const slow = new Promise((r) => setTimeout(r, 100, '遅いが成功'));

await Promise.race([fast, slow]);  // ❌ reject される(速い方が失敗だから)
await Promise.any([fast, slow]);   // ✅ '遅いが成功' が返る

「速さ」で決めるのがrace()、「成功」で決めるのがany()と覚えると混乱しません。フォールバックの用途でrace()を使うと、一番速く落ちたサーバーのエラーで全体が失敗します。

対応状況

環境 対応バージョン
Chrome / Edge 85
Firefox 79
Safari 14
Node.js 15

ES2021の一部として標準化されており、ポリフィルを入れる必要はもうありません。

まとめ

  • Promise.any()は最初に成功したPromiseの値を返す。全部失敗ならAggregateError
  • err.errorsに全ての失敗理由が配列で入る。空配列を渡した場合も即 reject
  • 本来の用途はミラー・CDNのフォールバック
  • Promise.resolve(false)は「成功」扱い。 判定に使うなら失敗をthrowで表す
  • 決着しても負けた処理は止まらない。AbortControllerでfinallyから中断する
  • race()は「速さ」、any()は「成功」で決まる
  • fetchと併用するときはres.okを必ず確認する(fetch() APIの使い方)

「どれか1つ返ってくればいい」という要件は意外と多く見つかります。tryを何段も重ねたフォールバック処理を書いているなら、Promise.any()で平らにできないか検討してみてください。