バッチ処理でフェイルファストせずエラーを収集して一括報告するTypeScriptパターン

バッチ処理でフェイルファストせずエラーを収集して一括報告するTypeScriptパターン

バッチ処理で1件のエラーが全体を止めないよう、エラーをMapに蓄積して処理完了後に一括報告するTypeScriptパターンを紹介します。検証メソッドがstring[]を返す設計と、エラーがある限り出力をブロックする仕組みにより、サイレントな失敗を防ぎます。
2026.07.25

はじめに

業務自動化のバッチ処理を作っていて、こんな場面に悩んだことはありませんか。

「100件のレコードを処理中に7件目でエラーが出た。このとき8〜100件目の処理を止めるべきか、続けるべきか?」

フェイルファスト (エラーが起きたら即中断) は安全ですが、他の93件が処理されないまま終わります。かといってエラーを無視して続行すると、問題に気づかないまま処理が走り続けます。

この記事では、「処理は止めない、でもエラーは全部把握する」というバランスを取るために使っているパターンを紹介します。

問題の具体例

複数のExcelファイルを読み込んで検証し、問題がなければ出力ファイルを作る処理を想像してください。

// ❌ フェイルファスト: 1件のエラーで全部止まる
for (const file of files) {
  const entry = readFile(file);
  if (!isValid(entry)) {
    throw new Error(`${file} に不備があります`); // ← ここで処理終了
  }
  processEntry(entry);
}

この実装では files[0] にエラーがあると files[1] 以降は処理されません。担当者が修正して再実行しても、今度は files[1] でエラーが出るかもしれない。ファイルが10本あれば最悪10回やり直しが発生します。

// ❌ エラー無視: 問題が検知されない
for (const file of files) {
  try {
    const entry = readFile(file);
    if (!isValid(entry)) continue; // ← エラーがあっても黙ってスキップ
    processEntry(entry);
  } catch {
    // 握りつぶし
  }
}

これはもっと悪い。何が起きているか分かりません。

解決策: エラー収集パターン

batch-error-collection-pattern-flow

Map<ファイル名, エラーメッセージ[]> にエラーを蓄積し、処理完了後にまとめて報告します。

class BatchProcessor {
  // エラー収集用のMap: キーはファイル名、値はエラーメッセージの配列
  private errorMap: Map<string, string[]> = new Map();
  private entryMap: Map<string, Entry[]> = new Map();

  public async main() {
    await this.processInputFiles();   // ← エラーがあっても続ける
    await this.broadcastErrors();     // ← エラーをまとめて報告
    await this.createOutput();        // ← エラーがなければ出力を作る
    this.clear();
  }

  private addErrorToMap(key: string, errors: string[]): void {
    if (this.errorMap.has(key)) {
      const existing = this.errorMap.get(key)!;
      this.errorMap.set(key, [...existing, ...errors]);
    } else {
      this.errorMap.set(key, [...errors]);
    }
  }
}

処理ループでのエラー収集

private async processInputFiles() {
  const promises = this.files.map(async (file) => {
    try {
      const workbook = await readXlsx(file);
      let rowNumber = 4;

      while (rowNumber <= workbook.rowCount) {
        const row = workbook.getRow(rowNumber);
        const entry = this.parseRow(row);

        // 検証してエラーがあればMapに追記、なければ処理対象に追加
        const isValid = await this.validate(file, entry, rowNumber);
        if (isValid) {
          this.addEntryToMap(file, entry);
        }

        rowNumber++;
      }
    } catch (error) {
      // ファイル単位の致命的なエラーもMapに収集
      Logger.error(`${file} の処理中にエラーが発生しました`);
    }
  });

  await Promise.all(promises); // 複数ファイルを並列処理
}

ポイント: isValidfalse でも rowNumber++ で次の行に進みます。エラーを投げないので、他のファイルや他の行の処理を止めません。

検証ロジックでのエラー収集

複数の検証ルールを並列で実行し、エラーをフラットな配列にまとめます。

private async validate(
  fileName: string,
  entry: Entry,
  row: number
): Promise<boolean> {
  // 8種類のチェックを並列実行
  const results = await Promise.all([
    this.requiredCheck(entry, row),     // 必須項目チェック
    this.lengthCheck(entry, row),       // 文字数チェック
    this.dateCheck(entry, row),         // 日付形式チェック
    this.timeCheck(entry, row),         // 時刻形式チェック
    this.phoneCheck(entry, row),        // 電話番号形式チェック
    this.postalCheck(entry, row),       // 郵便番号形式チェック
    this.addressCheck(entry, row),      // 住所チェック
  ]);

  // 各チェックの結果 (string[]) をすべて結合
  const errors = results.flat();

  if (errors.length > 0) {
    this.addErrorToMap(fileName, errors); // Mapに追記
    return false;
  }
  return true;
}

private async requiredCheck(entry: Entry, row: number): Promise<string[]> {
  const errors: string[] = [];
  const requiredFields = ["氏名", "電話番号", "住所", "郵便番号"];

  for (const field of requiredFields) {
    if (!entry[field]) {
      errors.push(`行${row}:${field}が空欄になっています。`);
    }
  }
  return errors;
}

各チェックメソッドが string[] を返す設計が重要です。エラーがなければ空配列 []、あればメッセージの配列を返します。throw しません。

エラーの一括報告と処理の分岐

private async broadcastErrors() {
  if (this.errorMap.size === 0) return; // エラーなし → スキップ

  const msg = [
    "ファイルに不備がありましたので修正をお願いします。\n",
  ];

  // ファイルごとにエラーをまとめて通知
  for (const [file, errors] of this.errorMap.entries()) {
    msg.push(`■ファイル名:${file}\n`, "---", ...errors);
  }

  await ChatNotifier.send(msg.join("\n")); // チャットツールに通知
}

private async createOutput(): Promise<void> {
  // エラーがあれば出力は作らない(中途半端な出力を防ぐ)
  if (this.errorMap.size !== 0) return;
  if (this.entryMap.size === 0) return;

  // 全件正常なときだけ出力を生成
  for (const [file, entries] of this.entryMap.entries()) {
    await this.generateOutputFile(file, entries);
  }
}

エラーがあれば出力ファイルを作らないという判断が大事です。「一部正常、一部エラー」の中途半端な状態で出力を作ると、後処理が複雑になります。エラーがある限り出力をブロックすることで、「このファイルが存在する = 全件正常」という保証が成り立ちます。

並列処理との組み合わせ

独立した複数のデータソースを処理する場合は、Promise.all と組み合わせます。

class SyncMarketData {
  // 各ステップの状態を個別に管理
  private step1 = { error: null as string | null, fileId: "" };
  private step2 = { error: null as string | null, fileId: "" };
  private step3 = { error: null as string | null, fileId: "" };
  private step4 = { error: null as string | null, fileId: "" };

  public async main() {
    try {
      // 4つのデータソースを並列処理
      await Promise.all([
        this.step1_syncShipping(),
        this.step2_syncArrival(),
        this.step3_syncLetterShipping(),
        this.step4_syncLetterArrival(),
      ]);

      await this.broadcastOutput();
    } finally {
      this.clear(); // 成功・失敗問わずキャッシュをクリア
    }
  }

  private async step1_syncShipping() {
    try {
      await this.fetchShippingData();
      await this.generateShippingCsv();
    } catch {
      // step1 のエラーを step1 のプロパティに記録
      // → step2〜4 の処理には影響しない
      this.step1.error = "出荷データ処理中にエラーが発生しました。";
    }
  }

  private async broadcastOutput() {
    const results = [this.step1, this.step2, this.step3, this.step4];
    const errors = results
      .filter((r) => r.error)
      .map((r) => r.error!);

    if (errors.length > 0) {
      await ChatNotifier.send(`エラーが発生しました:\n${errors.join("\n")}`);
    } else {
      await ChatNotifier.send("全ステップ正常完了");
    }
  }
}

step1 のエラーが step2〜4 をブロックしません。4つが並列で動き、すべて完了してから結果をまとめて報告します。

パターンのまとめ

このパターンを構成する3つの要素です。

1. エラー収集コンテナ
Map<キー, エラー[]> で収集します。throw に頼らず、エラーをデータとして扱います。

private errorMap: Map<string, string[]> = new Map();

private addError(key: string, errors: string[]) {
  const existing = this.errorMap.get(key) ?? [];
  this.errorMap.set(key, [...existing, ...errors]);
}

2. 検証メソッドは string[] を返す
エラーがなければ []、あればメッセージの配列。throw しません。呼び出し元が flat() で結合します。

// ✅ エラーをデータとして返す
async validate(entry: Entry, row: number): Promise<string[]> {
  const errors: string[] = [];
  if (!entry.name) errors.push(`行${row}: 氏名が空です`);
  return errors;
}

3. 出力は「全件正常」のときだけ生成
errorMap.size !== 0 なら出力をスキップします。中途半端な状態の出力を防ぎます。

private async createOutput() {
  if (this.errorMap.size !== 0) return; // エラーあり → スキップ
  // 全件正常なときだけ出力
}

フェイルファストとの使い分け

このパターンがすべての場面で正しいわけではありません。

状況 推奨
後続の処理がエラーデータに依存する フェイルファスト
各レコードが独立している エラー収集パターン
エラーの全体像を把握したい エラー収集パターン
部分的な成功が意味を持たない エラー収集パターン + 出力ブロック
エラーは即座に修正が必要 フェイルファスト

業務系のバッチ処理では「今日の全件をまとめて検証→修正→再実行」というサイクルが多いため、エラー収集パターンが合うことが多いです。

まとめ

  • エラーを throw するのではなく Map に蓄積することで、1件のエラーが全体を止めない
  • 検証メソッドは string[] を返す設計にして、結果を flat() でまとめる
  • エラーがある限り出力をブロックすることで「このファイルが存在する = 全件正常」の保証を作る
  • Promise.all と組み合わせることで、独立したステップを並列に動かしつつ個別のエラーを管理できる
  • 処理完了後にチャットツールへ一括通知することで、「サイレントな失敗」を防ぐ

エラーは「例外的な出来事」ではなく「処理結果のひとつ」として扱う、というのがこのパターンの核心です。

この記事をシェアする

関連記事