JavaScript | 非同期処理:Promise 応用 – Promise.race

JavaScript JavaScript
スポンサーリンク

Promise.race を一言でいうと

Promise.race は、
「複数の Promise のうち、“一番早く決着したやつだけ” の結果を採用する関数」 です。

race(レース)という名前の通り、
ゴールに一番最初に到着した Promise が「勝ち」で、
その成功 or 失敗が Promise.race 全体の結果になります。

ここが重要です。
Promise.all は「全員がゴールするまで待つ」
Promise.race は「最初にゴールした 1 人だけを見る」

というイメージで区別すると、直感的に理解しやすくなります。


基本の形と動き方

どう呼ぶか(シグネチャ)

形は Promise.allallSettled と同じく、
「Promise の配列」を渡します。

Promise.race([promise1, promise2, promise3])
  .then((value) => {
    // 最初に「成功」した Promise の値
  })
  .catch((error) => {
    // 最初に「失敗」した Promise のエラー
  });
JavaScript

動きはこうです。

一番最初に「fulfilled(成功)」または「rejected(失敗)」になった Promise の結果だけを採用し、
他の Promise の結果は 無視 します。

ここがポイントです。
「最初に成功したもの」だけではなく、「最初に失敗したもの」も勝ちます。
とにかく“一番早く settled(決着がついた)もの” が勝者になるレースです。

ごく簡単な例

const p1 = new Promise((resolve) => {
  setTimeout(() => resolve("p1"), 1000); // 1秒後
});

const p2 = new Promise((resolve) => {
  setTimeout(() => resolve("p2"), 500); // 0.5秒後
});

Promise.race([p1, p2]).then((value) => {
  console.log("勝ったのは:", value); // 勝ったのは: p2
});
JavaScript

この場合、

p1 → 1秒後に “p1” で成功
p2 → 0.5秒後に “p2” で成功

なので、p2 がレースに勝ち、
Promise.race 全体の結果は "p2" になります。


成功が勝つ場合と、失敗が勝つ場合

一番早く「成功」したケース

まずは「成功同士の勝負」から。

const fast = new Promise((resolve) => {
  setTimeout(() => resolve("fast"), 300); // 0.3秒
});

const slow = new Promise((resolve) => {
  setTimeout(() => resolve("slow"), 1000); // 1秒
});

Promise.race([fast, slow]).then((value) => {
  console.log("結果:", value); // 結果: fast
});
JavaScript

0.3 秒の fast が先に成功するので、
thenvalue"fast" になります。
1 秒後に来る slow はもう無視です。

一番早く「失敗」したケース

次に、失敗が勝つパターン。

const ok = new Promise((resolve) => {
  setTimeout(() => resolve("OK"), 1000);
});

const fail = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("早いエラー")), 300);
});

Promise.race([ok, fail])
  .then((value) => {
    console.log("成功:", value);
  })
  .catch((err) => {
    console.log("失敗:", err.message); // 失敗: 早いエラー
  });
JavaScript

300ms 後に fail がエラーになるので、
Promise.race は rejected と判断され、
catch"早いエラー" が渡されます。

このとき、
1 秒後に来るはずだった "OK" の結果は、
すでにレースが終わっているので採用されません。

ここが重要です。
Promise.race は「早い者勝ち」。
その「早い者」が成功か失敗かは関係なく、その結果だけが then/catch に届きます。


何に使うのか:典型パターンは「タイムアウト」

「一定時間待っても終わらないなら諦めたい」

Promise.race が特に力を発揮するのは、
「タイムアウト(時間制限)」を実装したいとき です。

イメージとしては、

サーバーからデータを取得する Promise
一定時間待っても終わらない場合に reject される「タイマー Promise」

この 2 つを Promise.race にかけて、
どちらが先に終わるかで挙動を変えます。

タイマー Promise を作る

まずは「n ミリ秒後に失敗する Promise」を作ります。

function timeout(ms) {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error(`${ms}ms タイムアウト`));
    }, ms);
  });
}
JavaScript

timeout(3000) は「3 秒待っても何もなければエラーになる Promise」です。

通信+タイムアウトを race させる

たとえば fetch で通信する場合(イメージです)。

function fetchWithTimeout(url, ms) {
  return Promise.race([
    fetch(url),     // 通信の Promise
    timeout(ms),    // タイムアウトの Promise
  ]);
}
JavaScript

使い方はこうです。

fetchWithTimeout("/api/data", 3000)
  .then((response) => {
    console.log("成功:", response);
  })
  .catch((err) => {
    console.error("失敗:", err.message);
  });
JavaScript

ここでの挙動は次のようになります。

ネットワークが速く、3 秒以内に fetch(url) が成功する
Promise.race は fetch の結果を採用し、then が呼ばれる

サーバーが遅くて 3 秒を超える
timeout(3000) のほうが先に reject される
Promise.race はタイムアウトエラーを採用し、catch が呼ばれる

ここが重要です。
Promise.race を使うと、「どちらが先か分からない 2 つ(または複数)の処理」のうち、“早く決着した方の結果だけを使う” という制御が簡単に書けるようになります。


複数の候補から「最初の成功」を採用したい場合

単純な race だと「早いエラー」が勝ってしまう

「複数のサーバーに同じリクエストを投げて、
どれか一つ成功したらその結果を使いたい」
というようなケースを考えてみましょう。

素直に Promise.race すると、

一番早いのが「失敗」だった場合、そのエラーで終わってしまう

という問題があります。

Promise.race([server1(), server2(), server3()])
  .then((res) => {
    console.log("成功:", res);
  })
  .catch((err) => {
    console.error("失敗:", err);
  });
JavaScript

もし server1 が一番早く「エラー」になった場合、
server2, server3 が後から成功しても、
race 全体はすでに「失敗」で終わっています。

「最初の成功」だけ欲しいなら、工夫が必要になる

「最初に settled(成功 or 失敗)したもの」ではなく、
「最初に成功したもの」 を採用したい場合は、
Promise.any(ES2021)という別のメソッドを使うか、
自前で工夫する必要があります。

ここでは詳細には踏み込みませんが、
race の性質として
「早い失敗も勝者になってしまう」
という点は覚えておいてください。

ここが重要です。
Promise.race=「最初に決着した 1 つの結果」であり、
「最初に成功した結果」ではない。
この違いを意識して使いどころを考えるのが大切です。


ちょっとした注意点とイメージの整理

他の Promise は「勝敗には関係なくなる」が、実行自体は止まらない

Promise.race は、一番早いやつを「結果として採用する」だけで、
残りの Promise を 強制停止するわけではありません

例えば、さっきの例:

const p1 = new Promise((resolve) => {
  setTimeout(() => {
    console.log("p1 実行完了");
    resolve("p1");
  }, 1000);
});

const p2 = new Promise((resolve) => {
  setTimeout(() => {
    console.log("p2 実行完了");
    resolve("p2");
  }, 500);
});

Promise.race([p1, p2]).then((value) => {
  console.log("race 結果:", value);
});
JavaScript

実行順は、

0.5秒後: “p2 実行完了” → race の勝者として "p2" が採用される
1.0秒後: “p1 実行完了”(ログは出るが、race の結果には影響しない)

のようになります。

つまり、
race が決着しても、他の非同期処理は裏で普通に動き続ける
ということです。

「本当に途中で止めたい」のなら、
AbortController や独自のキャンセル機構など、別の仕組みが必要になります。

配列の中身は Promise じゃなくてもよい

Promise.allallSettled と同様に、
race に渡す配列の中には「普通の値」も入れられます。

普通の値は「すぐに成功した Promise」として扱われるため、
ほぼ確実に勝ってしまいます。

Promise.race([
  123,
  new Promise((resolve) => setTimeout(() => resolve("遅い"), 1000)),
]).then((value) => {
  console.log(value); // 123
});
JavaScript

このように使うことはあまり多くありませんが、
「中身は全部 Promise でなければいけないわけではない」という柔軟性はあります。


初心者としての「Promise.race」の押さえどころ

最後に、本当に大事なポイントだけを整理します。

Promise.race([p1, p2, p3]) は、「p1, p2, p3 のうち、一番早く settled(成功 or 失敗)したものの結果だけを採用する」関数。

一番先に成功したら → then が呼ばれ、その値が渡る。
一番先に失敗したら → catch が呼ばれ、そのエラーが渡る。

典型的な用途は「タイムアウトの実装」:
「本物の処理」 vs 「時間切れエラー Promise」を race させて、早い方を採用する。

他の Promise は race の結果には使われなくなるが、実行自体は裏で続いている点に注意。

Promise.all は「全員ゴールを待つ」、
Promise.race は「誰か1人がゴールした瞬間にレース終了」。

ここが重要です。
Promise.race を「早い者勝ちルールを作るための道具」として捉えておくと、
「どっちが先に終わるか分からない処理」を扱うときに、とても強力な武器になります。

練習としては、

1秒後に resolve する Promise と、300ms 後に reject する Promise を用意して
Promise.race にかけてみる

それを自分で動かしてみて、
「早い失敗が勝って catch に来る」動きを目で確認すると、
Promise.race のイメージがしっかり定着するはずです。

タイトルとURLをコピーしました