バッチ処理でフェイルファストせずエラーを収集して一括報告するTypeScriptパターン
はじめに
業務自動化のバッチ処理を作っていて、こんな場面に悩んだことはありませんか。
「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 {
// 握りつぶし
}
}
これはもっと悪い。何が起きているか分かりません。
解決策: エラー収集パターン

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); // 複数ファイルを並列処理
}
ポイント: isValid が false でも 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と組み合わせることで、独立したステップを並列に動かしつつ個別のエラーを管理できる- 処理完了後にチャットツールへ一括通知することで、「サイレントな失敗」を防ぐ
エラーは「例外的な出来事」ではなく「処理結果のひとつ」として扱う、というのがこのパターンの核心です。







