1. ホーム
  2. JavaScript

【JavaScript】Promise.all・race・allSettled の使い方|複数の非同期処理をまとめて扱う

Share

複数の API からデータを取りに行く、画像を何枚もまとめて読み込む——こうした「いくつもの非同期処理を同時に走らせて、結果をまとめて受け取りたい」場面は実務でよく出てきます。このとき活躍するのが Promise の static メソッドです。この記事では Promise.allPromise.racePromise.allSettled の3つ(加えて Promise.any)を取り上げ、それぞれが「いつ resolve され、いつ reject されるのか」という決定的な違いを整理しながら、async/await と組み合わせた実用的な書き方まで解説します。Promise と async/await の基本はひととおり触ったことがある方を対象にしています。

1つずつ await すると遅くなる

まず、なぜまとめる仕組みが必要なのかを確認します。次のように複数の取得を await で順番に書くと、前の処理が終わるまで次が始まりません。3件それぞれ1秒かかるなら、合計でおよそ3秒かかってしまいます。

sequential.js
// 直列:前のfetchが終わってから次を始める(遅い)
async function loadSequential() {
  const user = await fetch('/api/user').then((r) => r.json());
  const posts = await fetch('/api/posts').then((r) => r.json());
  const tags = await fetch('/api/tags').then((r) => r.json());
  return { user, posts, tags };
}

3つの取得はお互いに依存していないので、本当は同時に走らせれば「いちばん遅いもの」が終わった時点で全部そろうはずです。この「同時に走らせて、まとめて待つ」を実現するのが Promise.all をはじめとする static メソッドです。これらは Promise の配列(正確には反復可能オブジェクト)を受け取り、新しい1つの Promise を返します。

4つのメソッドの違いを整理する

先に全体像をつかんでおきましょう。4つのメソッドは「いつ決着(settle)するか」と「結果として何を返すか」が異なります。次の表で見比べると違いがはっきりします。

メソッドresolve される条件reject される条件受け取る結果
Promise.allすべて成功したとき1つでも失敗した時点で即座に成功値の配列(入力順)
Promise.allSettled全部が決着したとき(必ず resolve)reject されない{status, value/reason} の配列
Promise.race最初に決着したのが成功のとき最初に決着したのが失敗のとき最初に決着した1件の値/理由
Promise.any最初に成功した1件が出たときすべて失敗したとき(AggregateError最初に成功した1件の値

ポイントは allallSettled が「全部を待つ」グループ、raceany が「最初の1件で決める」グループという対比です。さらに、失敗をどう扱うか(1つでも失敗したら諦めるのか、失敗も結果として受け取るのか)でそれぞれ性格が分かれます。ここからは1つずつコードで見ていきます。

Promise.all:すべて成功したら結果を配列で受け取る

もっともよく使うのが Promise.all です。渡したすべての Promise が成功すると、成功値を「渡した順番」に並べた配列で resolve されます。どれか1つでも reject されると、その時点で全体が即座に reject されます。冒頭の直列コードを Promise.all で書き直すと、3つの fetch が同時に走り、いちばん遅いものが終わった時点で結果がそろいます。

parallel.js
// 並列:3つのfetchを同時に走らせる
async function loadParallel() {
  const [user, posts, tags] = await Promise.all([
    fetch('/api/user').then((r) => r.json()),
    fetch('/api/posts').then((r) => r.json()),
    fetch('/api/tags').then((r) => r.json()),
  ]);
  // 結果は渡した順番で返る(先に終わった順ではない)
  return { user, posts, tags };
}

分割代入と相性が良いのも Promise.all の特徴です。返ってくる配列は「成功した順」ではなく「渡した順」なので、[user, posts, tags] のように受け取れば取り違える心配がありません。

動きを数字で確かめてみましょう。次のコードは、わざと待ち時間の違う Promise を3つ用意して Promise.all に渡しています。実行すると、全部がそろうまでの時間は合計(0.3+0.1+0.2秒)ではなく、いちばん遅い0.3秒で済むことがわかります。

all-demo.js
// 指定ミリ秒後にvalueでresolveするヘルパー
const wait = (ms, value) =>
  new Promise((resolve) => setTimeout(() => resolve(value), ms));

async function run() {
  const start = Date.now();
  const results = await Promise.all([
    wait(300, 'A'),
    wait(100, 'B'),
    wait(200, 'C'),
  ]);
  console.log(results);                 // ['A', 'B', 'C'](渡した順)
  console.log(Date.now() - start);      // およそ300(合計600ではない)
}

run();

1つでも失敗すると全体が止まる

Promise.all は「全員そろって成功」が条件です。1つでも reject されると、残りの結果を待たずにその理由で全体が reject されます。そのため try...catch で囲む必要があります。注意したいのは、reject されても他の Promise の処理自体が中断されるわけではない点です(裏では走り続けますが、結果は受け取れません)。「全部そろって初めて意味がある」データを取るときに Promise.all を選びます。

all-error.js
const reject = (ms, reason) =>
  new Promise((_, rej) => setTimeout(() => rej(new Error(reason)), ms));

async function run() {
  try {
    await Promise.all([
      wait(300, 'A'),
      reject(100, 'Bで失敗'),  // これが最初に失敗する
      wait(200, 'C'),
    ]);
  } catch (err) {
    // 100ms時点でここに来る。理由は最初に失敗したもの
    console.error(err.message); // 'Bで失敗'
  }
}

run();

Promise.allSettled:成功も失敗もまとめて受け取る

「一部が失敗しても、成功したぶんは使いたい」「どれが失敗したか一覧で知りたい」というときは Promise.allSettled を使います。こちらは渡したすべての Promise が決着するまで待ち、必ず resolve します(reject されません)。受け取る配列の各要素は、成功なら { status: 'fulfilled', value: ... }、失敗なら { status: 'rejected', reason: ... } という形のオブジェクトです。

allsettled-demo.js
async function run() {
  const results = await Promise.allSettled([
    wait(100, 'A'),
    reject(150, 'Bで失敗'),
    wait(200, 'C'),
  ]);

  // 失敗があってもcatchは不要。配列で全部の結果が返る
  results.forEach((result, i) => {
    if (result.status === 'fulfilled') {
      console.log(`${i}: 成功`, result.value);
    } else {
      console.log(`${i}: 失敗`, result.reason.message);
    }
  });
  // 0: 成功 A / 1: 失敗 Bで失敗 / 2: 成功 C
}

run();

成功した値だけを取り出したいときは、filterstatus を絞ってから mapvalue を取り出すと扱いやすくなります。たとえば「複数のニュースソースから記事を取得し、落ちたソースは無視して取れたぶんだけ表示する」といった、一部の失敗を許容したい処理にぴったりです。

allsettled-pick.js
const settled = await Promise.allSettled(sources.map((url) =>
  fetch(url).then((r) => r.json())
));

// 成功したものだけ値を取り出す
const articles = settled
  .filter((r) => r.status === 'fulfilled')
  .map((r) => r.value);

Promise.race:最初に決着した1件で決める

Promise.race(レース=競走)は、渡した中で最初に決着した1件の結果をそのまま採用します。成功・失敗は問わず、「いちばん早く決まったもの勝ち」です。最初に決着したのが成功なら resolve、失敗なら reject されます。

典型的な使い道が「タイムアウト」です。本来の処理と「一定時間後に reject するだけの Promise」をレースさせると、処理が遅すぎる場合にタイムアウト側が先に決着し、エラーとして扱えます。

race-timeout.js
// 指定ミリ秒で必ずrejectするタイムアウト用Promise
function timeout(ms) {
  return new Promise((_, reject) =>
    setTimeout(() => reject(new Error('タイムアウトしました')), ms)
  );
}

async function fetchWithTimeout(url, ms = 3000) {
  // fetchとタイムアウトを競走させる
  const response = await Promise.race([fetch(url), timeout(ms)]);
  return response.json();
}

// 3秒以内に応答がなければtimeout側が勝ち、catchに入る
try {
  const data = await fetchWithTimeout('/api/slow');
  console.log(data);
} catch (err) {
  console.error(err.message);
}

なお、近年の fetch では AbortControllersignal による中断のほうが「通信そのものを止められる」ぶん適していますが、Promise.race によるタイムアウトは fetch 以外の任意の Promise にも使える汎用的なテクニックとして覚えておくと便利です。

Promise.any:最初に成功した1件を採用する

Promise.anyrace と似ていますが、「最初の決着」ではなく「最初の成功」を待つ点が違います。失敗はスキップし、どれか1つでも成功すればその値で resolve します。すべて失敗したときだけ、各エラーをまとめた AggregateError で reject されます。複数のミラーサーバーから「どれでもいいので最初に成功したものを使いたい」といったケースで役立ちます。

any-demo.js
async function run() {
  try {
    // Bは早いが失敗、Cはやや遅いが成功 → Cが採用される
    const winner = await Promise.any([
      reject(100, 'Aで失敗'),
      reject(50, 'Bで失敗'),
      wait(200, 'C成功'),
    ]);
    console.log(winner); // 'C成功'
  } catch (err) {
    // 全部失敗したときだけここ。err は AggregateError
    console.error(err.errors); // 各Errorの配列
  }
}

run();

渡すのは Promise でなくてもよい

これらのメソッドに渡す配列の中身は、必ずしも Promise である必要はありません。Promise でない値が混ざっていると、自動的に「すぐ成功する Promise」として扱われます。そのため、すでに手元にある値と非同期で取りに行く値を混ぜて渡しても問題なく動きます。

mixed-values.js
const results = await Promise.all([
  42,                                   // ただの値もOK
  Promise.resolve('hello'),
  fetch('/api/user').then((r) => r.json()),
]);
// [42, 'hello', { ...userのJSON }]

並列にしたつもりが直列になっていないか

つまずきやすいのが、配列を作る前にうっかり await してしまうパターンです。次のように書くと、見た目は Promise.all を使っていても userA の取得が終わってから userB を取りに行くため、まったく並列になっていません。

not-parallel.js
// NG:配列を組み立てる前にawaitしているので直列になる
const userA = await fetch('/api/a').then((r) => r.json());
const userB = await fetch('/api/b').then((r) => r.json());
const both = await Promise.all([userA, userB]); // ここではもう取得済み

// OK:先にfetchを呼んでPromiseの状態にしてから配列にする
const both2 = await Promise.all([
  fetch('/api/a').then((r) => r.json()),
  fetch('/api/b').then((r) => r.json()),
]);

並列にするコツは、Promise.all に渡す前の段階では await せず、fetch を呼び出した「Promise のまま」配列に入れることです。呼び出した瞬間に通信が始まり、Promise.all がまとめて待ってくれます。もう1つの定番のミスは、map の中で非同期処理をするときに Promise.all で包み忘れることです。array.map(async ...) は Promise の配列を返すだけなので、そのまま使うと中身が解決されません。await Promise.all(array.map(async ...)) の形で待つ必要があります。

まとめ

複数の非同期処理をまとめる static メソッドは、「全部待つか/最初の1件で決めるか」と「失敗をどう扱うか」で選び分けます。Promise.all は全部成功してこそ意味があるデータの並列取得に使い、1つでも失敗すれば即 reject されるので try...catch で囲みます。一部の失敗を許容して結果を一覧で受け取りたいなら Promise.allSettled、最初に決着したもので決めるタイムアウト処理には Promise.race、最初に成功したものを採用したいときは Promise.any が適しています。並列化したいときは、配列を作る前に await しないことだけ気をつければ、async/await と組み合わせて読みやすく書けます。

参考ページ