バッチファイル(.bat / .cmd)でエラー処理を書いているはずなのに、実際にはエラーを見逃している、あるいは正常な処理をエラーとして誤検知してしまう ── そんなトラブルに心当たりはないでしょうか。
原因の多くは文法ミスではなく、ERRORLEVELの仕様や評価タイミングを正しく理解していないことにあります。バッチファイルの変数展開ルール、パイプやリダイレクトとの相互作用、コマンドごとに異なる終了コードの意味 ── これらを把握しないままエラー処理を書くと、テスト環境では動くのに本番で失敗する、という厄介な状況に陥ります。
本記事では、実務で特に遭遇しやすい15パターンの失敗例を、NG(失敗コード)→ 原因解説 → OK(修正コード)の三段構成で詳しく解説します。コピペして試せるサンプルコード付きなので、自分の環境で動作確認しながら読み進めてください。
- 前提知識: ERRORLEVELの基本仕様
- 失敗例1: ERRORLEVELを最後にまとめて判定してしまう
- 失敗例2: エラー判定の前に別コマンドを挟んでしまう
- 失敗例3: if ERRORLEVELを等号比較だと思い込んでいる
- 失敗例4: 括弧ブロック内で%ERRORLEVEL%を参照している
- 失敗例5: パイプ処理の結果をそのままエラー判定している
- 失敗例6: CALLなしでサブルーチンを呼んでしまう
- 失敗例7: exit と exit /b を混同してしまう
- 失敗例8: ファイルパスの空白に対応していない
- 失敗例9: 特殊文字のエスケープ忘れ
- 失敗例10: robocopyの終了コードを0/非0で判定している
- 失敗例11: findstr を単純なエラー判定に使っている
- 失敗例12: サブルーチンの戻り値を設計していない
- 失敗例13: SET /A のエラー処理を怠る
- 失敗例14: FORループ内のエラー処理が機能しない
- 失敗例15: バッチ連続実行時のエラー伝搬を考慮していない
- 失敗パターン早見表
- エラー処理を正しく書くための5つの原則
- よくある質問(FAQ)
- まとめ
- バッチファイルのエラーハンドリング一覧
前提知識: ERRORLEVELの基本仕様
失敗例に入る前に、ERRORLEVELに関する基本仕様を整理しておきます。ここを押さえていないと、なぜ失敗するのかが理解できません。
注意:ERRORLEVELという名前のユーザー環境変数を set ERRORLEVEL=0 のように定義すると、システムのERRORLEVELが参照できなくなります。set ERRORLEVEL= (値なし)で解除しない限り、すべてのエラー判定が狂うので要注意です。
失敗例1: ERRORLEVELを最後にまとめて判定してしまう
複数のコマンドを実行したあとに、最後にまとめてERRORLEVELを判定してしまうのは最も典型的な失敗パターンです。
NGコード
なぜ失敗するのか
ERRORLEVELは直前に実行されたコマンドの終了コードで上書きされます。蓄積はされません。上記の例では、仮に copy が失敗(ERRORLEVEL=1)しても、その後の del や mkdir が成功すれば ERRORLEVEL は 0 にリセットされます。結果として copy の失敗は一切検知されません。
OKコード(修正版)
ポイント:各コマンドの直後にERRORLEVELを判定するのが鉄則です。もう1つの方法として && 演算子でコマンドを連結し、途中で失敗した時点で後続を実行しない書き方もあります(例: copy source.txt dest.txt && del temp.txt && mkdir output)。
失敗例2: エラー判定の前に別コマンドを挟んでしまう
ログ出力やデバッグ用の echo を判定前に挟むことで、ERRORLEVELの参照対象が変わってしまうケースです。一見すると無害な echo が落とし穴になります。
NGコード
なぜ失敗するのか
echo コマンドは正常に実行されるとERRORLEVELを0にします。そのため、somecommand.exe が失敗しても、直後の echo によってERRORLEVELが0に上書きされ、エラー判定が常にスキップされます。
注意:echo以外にも、set、title、rem なども ERRORLEVELを変更する可能性があります。判定前には余計なコマンドを一切挟まないのが安全です。
OKコード(修正版)
ポイント:ERRORLEVELを変数に退避する方法もあります。set RESULT=%ERRORLEVEL% として保存すれば後からいつでも参照可能です。ただし set 自体がERRORLEVELに影響する場合もあるため、コマンド直後に行いましょう。
失敗例3: if ERRORLEVELを等号比較だと思い込んでいる
if ERRORLEVEL N の構文は、多くの初心者が「ERRORLEVELがNと等しいかどうか」を判定するものだと誤解しています。しかし実際には「ERRORLEVELがN以上かどうか」を判定する構文です。
NGコード
なぜ失敗するのか
if errorlevel 0 は「ERRORLEVELが0以上」という意味なので、ERRORLEVELが1のときでもTRUEになります。つまり、ERRORLEVELが1の場合は「エラー発生」と「正常終了」の両方が実行されてしまいます。
実行結果(ERRORLEVELが1のとき)
エラー発生 正常終了
「エラー発生」のあとに「正常終了」も表示されてしまい、判定が破綻しています。
OKコード(修正版)
失敗例4: 括弧ブロック内で%ERRORLEVEL%を参照している
バッチファイルで最もハマりやすい落とし穴の一つが、括弧ブロック(複合文)内での変数展開です。%変数%は括弧ブロック全体が読み込まれた時点で展開されるため、実行時の値が反映されません。
NGコード
なぜ失敗するのか
cmd.exeは括弧ブロック全体を一度にパースします。その際に %ERRORLEVEL% が展開されるため、somecommand.exe の実行前の値(通常は0)に置き換えられます。結果として、somecommand.exe がエラーを返しても常に「ERRORLEVEL = 0」と表示されます。
これは「遅延展開」の問題として知られており、括弧ブロック内で変数の変化を正しく追跡するには、setlocal enabledelayedexpansion と !変数! 構文を使う必要があります。
OKコード(修正版)
遅延展開が必要になるケース
- if / else の括弧ブロック内で変数を参照する場合
- for ループ内で変数の値が変化する場合
- 複合コマンド(&& や || で連結した行)の後半で参照する場合
注意:遅延展開を有効にすると、感嘆符(!)が特殊文字として扱われます。文字列中に ! を含むファイル名やデータを扱う場合は、^! でエスケープする必要があります。
失敗例5: パイプ処理の結果をそのままエラー判定している
パイプ(|)を使ったコマンドでは、ERRORLEVELはパイプの最後のコマンドの終了コードになります。前段のコマンドが失敗していても検知できません。
NGコード
なぜ失敗するのか
data.txt が存在しない場合、type はエラーを返しますが、パイプで接続された find は空の入力を受け取り、「見つからなかった」として ERRORLEVEL 1 を返します。問題は、type のエラーなのか find で見つからなかったのかが区別できない点です。
OKコード(修正版)
ポイント:パイプを使う場合は、前段の処理を事前チェックで切り離すか、一時ファイルに出力してから次の処理を実行するのが安全です。また、リダイレクト(>)でも同様の問題は起きにくいですが、2>&1 で標準エラーもまとめる習慣を付けておくとデバッグが容易です。
失敗例6: CALLなしでサブルーチンを呼んでしまう
バッチファイル内のサブルーチン(ラベル)を CALL なしで呼び出すと、制御がサブルーチンから戻ってこないという問題が発生します。
NGコード
なぜ失敗するのか
goto :DoWork は単にラベルにジャンプするだけで、戻り先を記録しません。そのため、サブルーチンの exit /b が実行されるとバッチファイル全体が終了し、「メイン処理に戻りました」以降のコードは一切実行されません。
一方、call :DoWork はサブルーチンの実行後に呼び出し元の次の行に戻ります。ERRORLEVELもサブルーチンの exit /b の値が引き継がれます。
OKコード(修正版)
失敗例7: exit と exit /b を混同してしまう
exit と exit /b はまったく異なる動作をします。この違いを理解していないと、エラー処理でバッチファイルだけでなくコマンドプロンプト自体が閉じてしまうという事故が起きます。
NGコード
なぜ失敗するのか
exit(/b なし)はcmd.exe プロセス自体を終了させます。コマンドプロンプトから手動で実行した場合はウィンドウが閉じ、別のバッチファイルから呼び出された場合は呼び出し元も巻き込んで終了します。
タスクスケジューラやCI/CDパイプラインから実行している場合は、後続のジョブがすべてスキップされるなど、影響が広範囲に及ぶことがあります。
OKコード(修正版)
失敗例8: ファイルパスの空白に対応していない
ファイルパスに空白が含まれる場合、ダブルクォートで囲まないとパスが途中で分断され、意図しないエラーが発生します。
NGコード
なぜ失敗するのか
%LOGDIR% が展開されると C:\Program Files\MyApp\logs になります。引用符がないため、cmd.exe はこれを C:\Program と Files\MyApp\logs の2つの引数として解釈します。
結果として if not exist C:\Program という意図しない判定になり、mkdir も copy も失敗します。エラーメッセージも「構文が誤っています」のように不親切で、原因の特定が困難です。
OKコード(修正版)
ポイント:set 文でも set "VAR=値" と全体をダブルクォートで囲む書き方が推奨されます。set VAR="値" と書くと、値にダブルクォートが含まれてしまうので注意してください。
失敗例9: 特殊文字のエスケープ忘れ
バッチファイルでは &、|、<、>、^、% などが特殊な意味を持ちます。これらをデータとして扱う際にエスケープを忘れると、予期しないエラーや動作になります。
NGコード
なぜ失敗するのか
set MSG=処理A&処理B完了 は、set MSG=処理A と 処理B完了 の2つのコマンドとして解釈されます。& はコマンドの区切り文字だからです。
同様に、括弧 () もバッチファイルでは特殊な意味(ブロック構文)を持つため、ファイル名に含まれていると構文エラーの原因になります。
OKコード(修正版)
失敗例10: robocopyの終了コードを0/非0で判定している
robocopy は他のコマンドと異なり、0以外の終了コードでも正常動作を意味します。これを知らずに「0以外はエラー」として処理すると、正常なファイルコピーをエラー扱いしてしまいます。
NGコード
なぜ失敗するのか
robocopy の終了コードはビットフラグで構成されており、0〜7は正常動作を示します。例えば「1」はファイルがコピーされたことを意味し、「0」は変更なし(コピー不要)を意味します。8以上が本当のエラーです。
OKコード(修正版)
ポイント:robocopy以外にも、xcopy(終了コード 0=正常, 4=コピー先の容量不足)やdsquery、net useなど、コマンドごとに終了コードの意味は異なります。エラー判定を書く前に、そのコマンドの終了コード仕様を確認する習慣をつけましょう。
失敗例11: findstr を単純なエラー判定に使っている
find や findstr は「文字列が見つからなかった」だけで ERRORLEVEL 1 を返します。これは処理の失敗ではなく、検索結果が0件であることを示しています。
NGコード
なぜ失敗するのか
findstr の ERRORLEVEL 1 は「一致する行が見つからなかった」を意味し、2が「ファイルが見つからなかった・構文エラー」です。上記コードでは、ファイルが存在してもSUCCESSの文字列がなければ「処理に失敗」と報告してしまいます。本当にやりたいのは「ログファイルの読み取りに失敗した」ケースの検出だけかもしれません。
OKコード(修正版)
失敗例12: サブルーチンの戻り値を設計していない
call で呼び出したサブルーチンで exit /b を明示しないと、ERRORLEVELが不定になり、呼び出し元のエラー判定が信頼できなくなります。
NGコード
なぜ失敗するのか
サブルーチン :Validate ではファイルが見つからない場合にエラーメッセージを表示しますが、exit /b 1 で終了コードを返していません。そのため、echoが成功してERRORLEVEL=0となり、呼び出し元では常に「バリデーション成功」と判定されます。
OKコード(修正版)
ポイント:サブルーチンを設計するときは、すべての分岐で exit /b 終了コード を明示しましょう。正常時は exit /b 0、エラー時は exit /b 1 以上の値を返します。引数には %~1(チルダ付き)を使うと、渡されたダブルクォートが自動的に除去されます。
失敗例13: SET /A のエラー処理を怠る
SET /A は算術演算を行うコマンドですが、構文エラーがあってもERRORLEVELが0のままという罠があります。
NGコード
実行結果
計算結果: 10
なぜ失敗するのか
SET /A は数値以外の文字列を0として扱うため、「abc + 10」は「0 + 10 = 10」と計算されてしまいます。ERRORLEVELも0のまま変化しないため、入力値の検証なしにはエラーを検出できません。
ゼロ除算の場合は標準エラーにメッセージが出ますが、それでもERRORLEVELは0のままです。
OKコード(修正版)
注意:SET /A はゼロ除算(set /a RESULT=10/0)でも ERRORLEVELを変更しません。標準エラーにメッセージが出力されるだけです。ゼロ除算を検出するには、除数が0でないことを事前にチェックする必要があります。
失敗例14: FORループ内のエラー処理が機能しない
FORループ内でのエラー処理は、失敗例4(遅延展開)と失敗例1(まとめて判定)の複合パターンです。ループ内でERRORLEVELを正しく取得するには、遅延展開が必須です。
NGコード
なぜ失敗するのか
FORループの括弧ブロックも、if文の括弧ブロックと同様にパース時に%変数%が展開されます。そのため、ループ開始前のERRORLEVEL値(通常0)がすべてのイテレーションで使われ、ループ内のcopyの結果が反映されません。
同様に %ERRORS% もパース時に展開されるため、最終結果は常に 0(初期値)になります。
OKコード(修正版)
ポイント:FORループ内では %変数% ではなく !変数! を使うのが鉄則です。ERRORLEVEL、ループカウンタ、フラグ変数など、ループ内で値が変化するすべての変数に !…! を使いましょう。
失敗例15: バッチ連続実行時のエラー伝搬を考慮していない
複数のバッチファイルを連続して呼び出す場合、前のバッチのERRORLEVELが次のバッチに引き継がれることがあります。これを考慮しないと、前回のエラーが残っていたために誤判定するケースが発生します。
NGコード
なぜ失敗するのか
CALLで呼ばれたバッチファイルの exit /b の値は、呼び出し元のERRORLEVELとして残ります。次のバッチファイルが、自身のコマンドを実行する前にERRORLEVELを参照すると、前回の値が見えてしまいます。
OKコード(修正版)
ポイント:ERRORLEVELをリセットする方法はいくつかあります。cmd /c "exit /b 0"、ver > nul(ver は常に0を返す)、(call )(空のcall)などが使えます。バッチの先頭でリセットする習慣を付けると安全です。
失敗パターン早見表
15のパターンを一覧で振り返ります。エラー処理を書くときのチェックリストとして活用してください。
エラー処理を正しく書くための5つの原則
15の失敗例を通じて見えてくる、バッチファイルのエラー処理における共通原則をまとめます。
5つの原則
- 原則1: 即座に判定する ── コマンド実行直後にERRORLEVELをチェック。間に何も挟まない
- 原則2: 遅延展開を理解する ── 括弧ブロック・FORループ内では !ERRORLEVEL! を使う
- 原則3: コマンドの仕様を確認する ── robocopy、findstr、xcopy など、コマンドごとに終了コードの意味が異なる
- 原則4: 戻り値を設計する ── サブルーチンはすべての分岐で exit /b N を明示する
- 原則5: 防御的に書く ── パスはダブルクォートで囲み、特殊文字はエスケープし、入力値を検証する
よくある質問(FAQ)
echo onでコマンドの実行内容を表示するか、pauseを要所に挿入して処理を止めて確認します。またecho %変数名%で変数の中身を確認するのも有効です。エラーが発生したらif %errorlevel% neq 0でエラーチェックポイントを設けると原因が特定しやすくなります。"C:\Program Files\myapp.exe"。変数を使う場合も"%PATH_VAR%"のようにクォートで囲みます。pauseコマンドを追加することで、「続行するには何かキーを押してください…」と表示されて止まります。または右クリック→「管理者として実行」ではなく、コマンドプロンプトを開いてからバッチファイルを実行する方法もあります。まとめ
バッチファイルのエラー処理で失敗する多くの原因は、ERRORLEVELの仕様を前提にせず「感覚的」に判定していることにあります。
エラー判定は、どのコマンドの結果を見ているのか、その終了コードは何を意味するのかを明確にしたうえで、直後に評価する設計が必要です。
特に遅延展開(setlocal enabledelayedexpansion + !変数!)は、括弧ブロックやFORループを使うバッチファイルでは避けて通れない知識です。本記事で紹介した15のパターンを把握しておけば、ERRORLEVELを「信用できない存在」ではなく「正しく使える判定材料」として活用できるようになります。
エラー処理は地味な作業ですが、バッチファイルの信頼性を大きく左右します。一度正しいパターンを身につければ、以後のトラブルを大幅に減らせるはずです。
バッチファイルのエラーハンドリング一覧
目的や状況別に、エラー処理の方法を整理しています。

