JavaScript Tips | 配列ユーティリティ:Promise 配列制御

JavaScript JavaScript
スポンサーリンク

テーマの整理:「Promise 配列制御」とは何か

「Promise 配列制御」というのは、ざっくり言うと
「複数の非同期処理(Promise)を、配列としてまとめて扱い、どう待つか・どう制御するかを決めるテクニック」です。

業務だと、こんな状況がよく出てきます。

複数ユーザーの情報を API でまとめて取得したい。
複数ファイルを並列でアップロードしたい。
複数のチェック処理を投げて、全部終わってから結果を集計したい。

こういうとき、Promise をバラバラに扱うのではなく、
「Promise の配列」としてまとめて制御できると、コードが一気に整理されます。

ここでは、実務でよく使うユーティリティ的な考え方を、初心者向けにかみ砕いて説明していきます。


Promise.all 系の基本ユーティリティ

Promise.all:全部成功したら結果を配列で返す

Promise.all は、「全部の Promise が成功したら、結果を配列で返す」関数です。
1 つでも失敗(reject)すると、全体が reject になります。

function all(promises) {
  return Promise.all(promises);
}
JavaScript

これはラッパーというより「名前を短くしただけ」ですが、
「Promise 配列をまとめて待つ」という意味をはっきりさせるために、
自分のユーティリティとして all という名前で使うのもアリです。

例題として、複数ユーザーを並列で取得するコードを見てみます。

async function fetchUser(id) {
  // 実際には fetch などで API を叩く想定
  await new Promise((r) => setTimeout(r, 200));
  return { id, name: `User-${id}` };
}

async function main() {
  const ids = [1, 2, 3];

  const promises = ids.map((id) => fetchUser(id));

  const users = await all(promises);

  console.log(users);
  // [{ id:1, ... }, { id:2, ... }, { id:3, ... }]
}

main();
JavaScript

ここで重要なのは、
「Promise の配列を作る」→「all で一気に待つ」というパターンを体で覚えることです。

Promise.allSettled:成功・失敗を全部知りたいとき

Promise.allSettled は、「全部の Promise が終わるまで待ち、成功か失敗かを含めて結果を返す」関数です。
途中で失敗しても、全体は reject せず、最後まで待ちます。

ユーティリティとしては、例えばこうラップしておくと分かりやすくなります。

function allSettled(promises) {
  return Promise.allSettled(promises);
}
JavaScript

例題として、「成功したものだけを取り出す」処理を書いてみます。

async function maybeFail(id) {
  await new Promise((r) => setTimeout(r, 200));
  if (id % 2 === 0) {
    throw new Error("failed: " + id);
  }
  return { id, ok: true };
}

async function main() {
  const ids = [1, 2, 3, 4];

  const promises = ids.map((id) => maybeFail(id));
  const results = await allSettled(promises);

  const fulfilled = results
    .filter((r) => r.status === "fulfilled")
    .map((r) => r.value);

  console.log(fulfilled);
  // [{ id:1, ok:true }, { id:3, ok:true }]
}

main();
JavaScript

重要ポイントは、
「失敗しても全体が落ちない」「成功・失敗を後から自分で仕分けできる」ことです。
業務では、「一部失敗しても処理を続けたい」ケースが多いので、allSettled 系のユーティリティはかなり実用的です。


race / any 系のユーティリティ

Promise.race:一番早く終わったものだけ欲しい

Promise.race は、「最初に settle(成功 or 失敗)した Promise の結果だけを返す」関数です。
タイムアウト処理などでよく使われます。

ユーティリティとしては、例えばこうです。

function race(promises) {
  return Promise.race(promises);
}
JavaScript

例題として、「API とタイムアウトのどちらが先か」を見るコードを書いてみます。

async function fetchSlow() {
  await new Promise((r) => setTimeout(r, 1000));
  return "OK";
}

function timeout(ms) {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error("timeout")), ms);
  });
}

async function main() {
  try {
    const result = await race([fetchSlow(), timeout(500)]);
    console.log(result);
  } catch (e) {
    console.log("error:", e.message); // "timeout"
  }
}

main();
JavaScript

「Promise の配列を race に渡すと、一番早く終わったものだけが採用される」という感覚を持っておくと、
タイムアウトやフォールバック処理をきれいに書けるようになります。

Promise.any:どれか 1 つでも成功すれば OK

Promise.any は、「どれか 1 つでも成功したら、その結果を返す」関数です。
全部失敗したときだけ reject します。

ユーティリティとしては、こうラップできます。

function any(promises) {
  return Promise.any(promises);
}
JavaScript

例題として、「複数のエンドポイントのうち、どれか 1 つでも応答してくれればいい」というケースを考えます。

async function fetchFromA() {
  await new Promise((r) => setTimeout(r, 800));
  return "A";
}

async function fetchFromB() {
  await new Promise((r) => setTimeout(r, 300));
  return "B";
}

async function main() {
  const result = await any([fetchFromA(), fetchFromB()]);
  console.log(result); // "B"
}

main();
JavaScript

「どれか 1 つ成功すればいい」という要件は意外と多いので、
any という名前でユーティリティ化しておくと、意図が読みやすくなります。


同時実行数を制御するユーティリティ(Promise 配列制御の“本命”)

なぜ「同時実行数」が重要なのか

Promise を配列で扱うとき、
「全部一気に実行する(完全並列)」か「1 個ずつ順番に実行する(完全直列)」か、
このどちらかだけだと、現実の業務では困ることが多いです。

API のレート制限があるので、一度に大量に叩きたくない。
でも、完全に 1 個ずつだと遅すぎる。

そこで、「同時に動かす Promise の数を制限する」ユーティリティがとても役に立ちます。

実装例:runWithLimit(最大 N 個まで並列実行)

async function runWithLimit(tasks, limit = 5) {
  if (!Array.isArray(tasks)) {
    return [];
  }
  if (typeof limit !== "number" || limit <= 0) {
    limit = 1;
  }

  const results = new Array(tasks.length);
  let currentIndex = 0;

  async function worker() {
    while (currentIndex < tasks.length) {
      const index = currentIndex;
      currentIndex += 1;

      const task = tasks[index];
      try {
        results[index] = await task();
      } catch (e) {
        results[index] = e;
      }
    }
  }

  const workers = [];
  const workerCount = Math.min(limit, tasks.length);
  for (let i = 0; i < workerCount; i++) {
    workers.push(worker());
  }

  await Promise.all(workers);
  return results;
}
JavaScript

ここでの重要ポイントを丁寧に分解します。

tasks は「関数の配列」です。
各関数は呼び出されると Promise を返す、つまり「まだ実行していない非同期処理」です。
currentIndex で「次に実行すべきタスクの位置」を共有します。
worker は「タスクを取りに行って実行する役割」を持つ非同期関数です。
worker を limit 個だけ立ち上げることで、「同時に動くタスクの数」を制限します。
全部の worker が終わったら、results に全タスクの結果が入っています。

これが、Promise 配列制御の中でもかなり実務的なパターンです。

例題:最大 3 並列で API を叩く

async function fetchUser(id) {
  console.log("start", id);
  await new Promise((r) => setTimeout(r, 500));
  console.log("end", id);
  return { id, name: `User-${id}` };
}

async function main() {
  const ids = [1, 2, 3, 4, 5, 6];

  const tasks = ids.map((id) => () => fetchUser(id));

  const results = await runWithLimit(tasks, 3);

  console.log(results);
}

main();
JavaScript

ログを眺めると、「常に最大 3 つまでしか同時に動いていない」ことが分かるはずです。
これがまさに「Promise 配列を、同時実行数という観点で制御している」状態です。


Promise 配列制御で意識してほしい“設計の軸”

軸 1:いつ全部待つか、いつ一部だけでいいか

全部終わるまで待ちたいなら allallSettled
一番早いものだけ欲しいなら race
どれか 1 つ成功すればいいなら any

「この処理は、どのタイミングで“完了”とみなしたいのか?」を、
関数名レベルで表現しておくと、コードを読む人が迷いません。

軸 2:同時実行数をどうするか

完全並列でいいのか。
完全直列にしたいのか。
最大 N 個まで並列にしたいのか。

これも、runWithLimit のようなユーティリティ名に意図を埋め込んでおくと、
「この処理は負荷を意識しているんだな」と一目で分かります。

軸 3:失敗をどう扱うか

1 つでも失敗したら全体を失敗にするのか(all 的)。
失敗しても最後まで全部見てから判断したいのか(allSettled 的)。
失敗したものは結果配列にエラーとして入れておくのか。

業務では、「一部失敗は許容するが、ログには残したい」などの要件が多いので、
「エラーをどう扱うか」も Promise 配列制御の一部として設計しておくと良いです。


手を動かして「Promise 配列制御」の感覚をつかむ

まとめて試せる小さなサンプルを書きます。
Node などで実行して、ログの出方を眺めてみてください。

function all(promises) {
  return Promise.all(promises);
}

function allSettled(promises) {
  return Promise.allSettled(promises);
}

function race(promises) {
  return Promise.race(promises);
}

function any(promises) {
  return Promise.any(promises);
}

async function runWithLimit(tasks, limit = 2) {
  if (!Array.isArray(tasks)) return [];
  if (typeof limit !== "number" || limit <= 0) limit = 1;

  const results = new Array(tasks.length);
  let currentIndex = 0;

  async function worker() {
    while (currentIndex < tasks.length) {
      const index = currentIndex;
      currentIndex += 1;
      const task = tasks[index];
      try {
        results[index] = await task();
      } catch (e) {
        results[index] = e;
      }
    }
  }

  const workers = [];
  const workerCount = Math.min(limit, tasks.length);
  for (let i = 0; i < workerCount; i++) {
    workers.push(worker());
  }

  await Promise.all(workers);
  return results;
}

async function demo() {
  const ids = [1, 2, 3, 4];

  console.log("=== all ===");
  console.log(
    await all(
      ids.map(async (id) => {
        await new Promise((r) => setTimeout(r, 200 * id));
        return id * 10;
      })
    )
  );

  console.log("=== allSettled ===");
  console.log(
    await allSettled(
      ids.map(async (id) => {
        await new Promise((r) => setTimeout(r, 200 * id));
        if (id % 2 === 0) throw new Error("fail " + id);
        return id * 10;
      })
    )
  );

  console.log("=== race ===");
  console.log(
    await race(
      ids.map(async (id) => {
        await new Promise((r) => setTimeout(r, 200 * id));
        return id;
      })
    )
  );

  console.log("=== any ===");
  console.log(
    await any(
      ids.map(async (id) => {
        await new Promise((r) => setTimeout(r, 200 * id));
        if (id < 3) throw new Error("fail " + id);
        return id;
      })
    )
  );

  console.log("=== runWithLimit (limit=2) ===");
  const tasks = ids.map((id) => async () => {
    console.log("start", id);
    await new Promise((r) => setTimeout(r, 500));
    console.log("end", id);
    return id * 100;
  });
  console.log(await runWithLimit(tasks, 2));
}

demo();
JavaScript

「どの関数がどんなふうに Promise 配列を制御しているか」を、
ログと結果を見ながら体で覚えていくと、実務での使いどころが見えてきます。


まとめ:Promise 配列制御は「非同期を怖くなくするための設計」

配列ユーティリティとしての「Promise 配列制御」は、
単に便利な関数というより、「非同期処理をどう設計するか」という考え方そのものです。

全部待つのか、一部でいいのか。
全部並列なのか、順番なのか、制限付き並列なのか。
失敗をどう扱うのか。

これらをユーティリティ関数の名前と振る舞いに落とし込んでおくと、
「このコードは何を意図しているのか」が、読み手にちゃんと伝わるようになります。

そこまでいくと、非同期処理はもう“怖いもの”ではなく、
「ちゃんとコントロールできる道具」になっていきます。

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