Promise.allで独立したバッチ処理ステップを並列化しつつ個別のエラーを管理する

Promise.allで独立したバッチ処理ステップを並列化しつつ個別のエラーを管理する

バッチ処理で独立した複数ステップを Promise.all で並列実行しつつ、各ステップのエラーをインスタンスプロパティで個別管理するパターンを紹介します。Promise.allSettled との使い分けも解説。
2026.07.28

はじめに

バッチ処理で複数のデータソースを処理するとき、こういった状況があります。

  • 「売上データ」「在庫データ」「配送ステータス」「請求データ」の4種類を毎日更新する
  • 4つは互いに独立している(順番依存なし、データの依存なし)
  • でも1つが失敗したとき、他の3つを止めたくない

単純に順番に実行すると「4ステップ × 処理時間」かかります。Promise.all で並列化できますが、1つが throw すると残りが中断される問題があります。

この記事では、独立したステップを並列実行しつつ、各ステップのエラーを独立して管理するパターンを紹介します。

素朴な実装の問題

// ❌ 逐次実行: 合計時間が長い
public async main() {
  await this.step1_売上データ同期();     // 30秒
  await this.step2_在庫データ更新();     // 30秒
  await this.step3_配送ステータス更新(); // 30秒
  await this.step4_請求データ更新();     // 30秒
  // 合計: 約120秒
}
// ❌ Promise.all: 1つのエラーで全部キャンセル
public async main() {
  await Promise.all([
    this.step1(),  // step1 が throw すると...
    this.step2(),  // step2, 3, 4 の結果が無視される
    this.step3(),
    this.step4(),
  ]);
}

Promise.all は最初のrejectで即座に全体が失敗します。step1がエラーになったとき、step2〜4は実行され続けますが、結果を取得できないまま終わります。

解決策: 各ステップが自分のエラーを保持する

nodejs-parallel-independent-steps-pattern-error-flow

各ステップの結果(成功・失敗・出力ファイルID)をインスタンスプロパティで管理します。ステップ内でエラーをキャッチしてプロパティに記録するので、throw が外に出ません。

日次データ同期.ts
class SyncDailyReports {
  // 各ステップの状態を個別に管理
  private 売上データ同期 = { error: null as string | null, fileId: "" };
  private 在庫データ更新 = { error: null as string | null, fileId: "" };
  private 配送ステータス更新 = { error: null as string | null, fileId: "" };
  private 請求データ更新 = { error: null as string | null, fileId: "" };

  public async main() {
    Logger.task("【処理開始】日次データ同期");

    try {
      // 4ステップを並列実行 — 1つが失敗しても他は続く
      await Promise.all([
        this.step1_売上データ同期(),
        this.step2_在庫データ更新(),
        this.step3_配送ステータス更新(),
        this.step4_請求データ更新(),
      ]);

      await this.broadcastOutput(); // 全ステップ完了後に結果をまとめて通知
    } finally {
      this.キャッシュクリア();
    }
  }

  private async step1_売上データ同期() {
    try {
      await this.get売上データ更新対象();
      await this.create売上データCSV();
      Logger.success("【ステップ1完了】売上データ同期");
    } catch (error) {
      // エラーをプロパティに記録するだけ — throw しない
      this.売上データ同期.error = "売上データ処理中にエラーが発生しました。";
      Logger.error("【ステップ1失敗】売上データ同期");
    }
    // ↑ catch を抜けても Promise は resolved — Promise.all はキャンセルされない
  }

  private async step2_在庫データ更新() {
    try {
      await this.get在庫データ更新対象();
      await this.create在庫データCSV();
      Logger.success("【ステップ2完了】在庫データ更新");
    } catch {
      this.在庫データ更新.error = "在庫データ処理中にエラーが発生しました。";
      Logger.error("【ステップ2失敗】在庫データ更新");
    }
  }

  // step3, step4 も同様...
}

ステップメソッドの中で catch してプロパティに記録するので、Promiseは常に resolved で完了します。Promise.all は全4ステップが完了(resolve or catch後のresolve)するまで待ちます。

結果の一括通知

全ステップ完了後、各ステップの状態を確認して通知します。

private async broadcastOutput() {
  const steps = [
    { name: "売上データ同期", state: this.売上データ同期 },
    { name: "在庫データ更新", state: this.在庫データ更新 },
    { name: "配送ステータス更新", state: this.配送ステータス更新 },
    { name: "請求データ更新", state: this.請求データ更新 },
  ];

  const errors = steps
    .filter((s) => s.state.error)
    .map((s) => `❌ ${s.name}: ${s.state.error}`);

  const successes = steps
    .filter((s) => !s.state.error)
    .map((s) => `✅ ${s.name}`);

  const msg = [
    "【日次データ同期完了】",
    ...successes,
    ...(errors.length > 0 ? ["", "--- エラー ---", ...errors] : []),
  ].join("\n");

  await Notification.send(msg);
}

4ステップすべて正常なら「✅ 4件完了」、2件失敗なら「✅ 2件完了 + ❌ 2件エラー」のような通知が届きます。

実行時間の比較

nodejs-parallel-independent-steps-pattern-timeline

このパターンの実際の効果です。各ステップが外部API呼び出しを含み、それぞれ約30秒かかる場合:

逐次実行: 30 + 30 + 30 + 30 = 約120秒

並列実行: max(30, 30, 30, 30) = 約30秒

4つが完全に独立している場合、理論上は1ステップ分の時間で全体が完了します。

Promise.allSettled との違い

Node.js標準には Promise.allSettled という、すべてのPromiseが完了するまで(reject でも)待つ API があります。

// Promise.allSettled を使うアプローチ
const results = await Promise.allSettled([
  this.step1(),
  this.step2(),
  this.step3(),
  this.step4(),
]);

results.forEach((result, i) => {
  if (result.status === "rejected") {
    console.error(`Step ${i + 1} failed:`, result.reason);
  }
});

これも有効なアプローチですが、今回のパターンとの違いがあります:

比較 Promise.allSettled インスタンスプロパティ管理
エラー情報の持ち方 result.reason this.step1.error
後処理でのアクセス results[0] のインデックス this.step1 の名前付き参照
型安全性 PromiseSettledResult<T> 独自の型定義
エラー内容の加工 catch 内でできる プロパティへの代入で自由に加工

ステップ数が少なくエラー処理がシンプルなら Promise.allSettled が簡潔です。ステップ数が多い、エラーメッセージを加工したい、出力ファイルのIDなど追加の状態も管理したい場合はインスタンスプロパティ管理の方が整理しやすいです。

パターンのポイントまとめ

// 各ステップの状態を型で明確に定義する
private step1 = { error: null as string | null, fileId: "" };

// ステップメソッドはエラーをキャッチしてプロパティに記録
private async step1_処理() {
  try {
    // 処理...
  } catch {
    this.step1.error = "処理中にエラーが発生しました。";
  }
  // throw しない → Promise は常に resolved
}

// Promise.all で並列実行 → 全ステップ完了を待つ
await Promise.all([
  this.step1_処理(),
  this.step2_処理(),
]);

// 完了後に状態を確認して通知
await this.結果通知();

// 必ずクリア
this.キャッシュクリア();

独立したステップは並列に: 順番依存がないステップを await で順番に書く理由はない。

エラーはプロパティに記録: throw を外に出さないことで Promise.all のキャンセルを防ぐ。

全ステップ後にまとめて通知: 1ステップごとに通知するより、全体の状況をまとめて見せる方がわかりやすい。

この記事をシェアする

関連記事