【bat】バッチファイルのエラー処理でよくある15の失敗例と正しい書き方

【bat】バッチファイルのエラー処理でよくある失敗例 bat
TypeScriptのよくあるエラーメッセージと解決方法を完全ガイド。TS2322・TS2345・TS2339・TS2532・TS7006・TS2304・TS2769・TS2741・TS2352・TS18046・TS2349等のエラーコードごとに原因・NGコード・OKコード対比で分かりやすく解説。tsconfig設定ミス、型定義ファイル関連エラー、デバッグテクニックまで網羅。

バッチファイル(.bat / .cmd)でエラー処理を書いているはずなのに、実際にはエラーを見逃している、あるいは正常な処理をエラーとして誤検知してしまう ── そんなトラブルに心当たりはないでしょうか。

原因の多くは文法ミスではなく、ERRORLEVELの仕様や評価タイミングを正しく理解していないことにあります。バッチファイルの変数展開ルール、パイプやリダイレクトとの相互作用、コマンドごとに異なる終了コードの意味 ── これらを把握しないままエラー処理を書くと、テスト環境では動くのに本番で失敗する、という厄介な状況に陥ります。

本記事では、実務で特に遭遇しやすい15パターンの失敗例を、NG(失敗コード)→ 原因解説 → OK(修正コード)の三段構成で詳しく解説します。コピペして試せるサンプルコード付きなので、自分の環境で動作確認しながら読み進めてください。

No. 失敗パターン 核心
1 ERRORLEVELを最後にまとめて判定 直前の結果しか残らない
2 判定前に別コマンドを挟む echoでERRORLEVELが変わる
3 if ERRORLEVELの比較仕様の誤解 「以上」比較であること
4 括弧ブロック内で%ERRORLEVEL%参照 遅延展開が必要
5 パイプ処理の結果をそのまま判定 最後のコマンドの結果のみ
6 CALLなしでサブルーチンを呼ぶ 制御が戻らない
7 exitとexit /bの混同 cmd.exeごと終了する
8 ファイルパスの空白未対応 引用符で囲む必要
9 特殊文字のエスケープ忘れ &, |, <, >, ^ の扱い
10 robocopyの終了コード誤判定 0以外=エラーではない
11 findstrを単純エラー判定に使う 見つからない=ERRORLEVEL 1
12 サブルーチンの戻り値が未設計 exit /bで明示的に返す
13 SET /Aのエラー処理 構文エラーでもERRORLEVEL=0
14 FORループ内のエラー処理 遅延展開+即座判定が必要
15 バッチ連続実行時のエラー伝搬 前回のERRORLEVELが残る
スポンサーリンク
  1. 前提知識: ERRORLEVELの基本仕様
  2. 失敗例1: ERRORLEVELを最後にまとめて判定してしまう
    1. NGコード
    2. なぜ失敗するのか
    3. OKコード(修正版)
  3. 失敗例2: エラー判定の前に別コマンドを挟んでしまう
    1. NGコード
    2. なぜ失敗するのか
    3. OKコード(修正版)
  4. 失敗例3: if ERRORLEVELを等号比較だと思い込んでいる
    1. NGコード
    2. なぜ失敗するのか
    3. OKコード(修正版)
  5. 失敗例4: 括弧ブロック内で%ERRORLEVEL%を参照している
    1. NGコード
    2. なぜ失敗するのか
    3. OKコード(修正版)
  6. 失敗例5: パイプ処理の結果をそのままエラー判定している
    1. NGコード
    2. なぜ失敗するのか
    3. OKコード(修正版)
  7. 失敗例6: CALLなしでサブルーチンを呼んでしまう
    1. NGコード
    2. なぜ失敗するのか
    3. OKコード(修正版)
  8. 失敗例7: exit と exit /b を混同してしまう
    1. NGコード
    2. なぜ失敗するのか
    3. OKコード(修正版)
  9. 失敗例8: ファイルパスの空白に対応していない
    1. NGコード
    2. なぜ失敗するのか
    3. OKコード(修正版)
  10. 失敗例9: 特殊文字のエスケープ忘れ
    1. NGコード
    2. なぜ失敗するのか
    3. OKコード(修正版)
  11. 失敗例10: robocopyの終了コードを0/非0で判定している
    1. NGコード
    2. なぜ失敗するのか
    3. OKコード(修正版)
  12. 失敗例11: findstr を単純なエラー判定に使っている
    1. NGコード
    2. なぜ失敗するのか
    3. OKコード(修正版)
  13. 失敗例12: サブルーチンの戻り値を設計していない
    1. NGコード
    2. なぜ失敗するのか
    3. OKコード(修正版)
  14. 失敗例13: SET /A のエラー処理を怠る
    1. NGコード
    2. なぜ失敗するのか
    3. OKコード(修正版)
  15. 失敗例14: FORループ内のエラー処理が機能しない
    1. NGコード
    2. なぜ失敗するのか
    3. OKコード(修正版)
  16. 失敗例15: バッチ連続実行時のエラー伝搬を考慮していない
    1. NGコード
    2. なぜ失敗するのか
    3. OKコード(修正版)
  17. 失敗パターン早見表
  18. エラー処理を正しく書くための5つの原則
  19. よくある質問(FAQ)
  20. まとめ
  21. バッチファイルのエラーハンドリング一覧

前提知識: ERRORLEVELの基本仕様

失敗例に入る前に、ERRORLEVELに関する基本仕様を整理しておきます。ここを押さえていないと、なぜ失敗するのかが理解できません。

仕様 説明
上書き方式 ERRORLEVELは直前に実行したコマンドの終了コードで上書きされる。蓄積はされない
if ERRORLEVEL N 「ERRORLEVELがN以上」のときTRUE。等号比較ではない
%ERRORLEVEL% 環境変数として値を取得する書式。等号比較が可能だが、括弧ブロック内では遅延展開が必要
遅延展開 setlocal enabledelayedexpansion で有効化し、!ERRORLEVEL! で参照する
コマンドごとの差異 robocopyは0〜7が正常、findstrは不一致で1を返す。コマンドにより終了コードの意味が異なる

注意:ERRORLEVELという名前のユーザー環境変数を set ERRORLEVEL=0 のように定義すると、システムのERRORLEVELが参照できなくなります。set ERRORLEVEL= (値なし)で解除しない限り、すべてのエラー判定が狂うので要注意です。

失敗例1: ERRORLEVELを最後にまとめて判定してしまう

複数のコマンドを実行したあとに、最後にまとめてERRORLEVELを判定してしまうのは最も典型的な失敗パターンです。

NGコード

NG: まとめて判定
@echo off

rem 3つのコマンドを順に実行
copy source.txt dest.txt
del temp.txt
mkdir output

rem 最後にまとめてエラーチェック(NG)
if %ERRORLEVEL% neq 0 (
    echo エラーが発生しました
    exit /b 1
)
echo すべて正常に完了しました

なぜ失敗するのか

ERRORLEVELは直前に実行されたコマンドの終了コードで上書きされます。蓄積はされません。上記の例では、仮に copy が失敗(ERRORLEVEL=1)しても、その後の del や mkdir が成功すれば ERRORLEVEL は 0 にリセットされます。結果として copy の失敗は一切検知されません。

OKコード(修正版)

OK: コマンドごとに即座に判定
@echo off

copy source.txt dest.txt
if %ERRORLEVEL% neq 0 (
    echo [ERROR] copy に失敗しました
    exit /b 1
)

del temp.txt
if %ERRORLEVEL% neq 0 (
    echo [ERROR] del に失敗しました
    exit /b 1
)

mkdir output
if %ERRORLEVEL% neq 0 (
    echo [ERROR] mkdir に失敗しました
    exit /b 1
)

echo すべて正常に完了しました

ポイント:各コマンドの直後にERRORLEVELを判定するのが鉄則です。もう1つの方法として && 演算子でコマンドを連結し、途中で失敗した時点で後続を実行しない書き方もあります(例: copy source.txt dest.txt && del temp.txt && mkdir output)。

失敗例2: エラー判定の前に別コマンドを挟んでしまう

ログ出力やデバッグ用の echo を判定前に挟むことで、ERRORLEVELの参照対象が変わってしまうケースです。一見すると無害な echo が落とし穴になります。

NGコード

NG: echoが判定対象を変えてしまう
@echo off

somecommand.exe
echo 処理が完了しました

rem ここでの判定対象は somecommand ではなく echo
if errorlevel 1 (
    echo エラーが発生しました
    exit /b 1
)

なぜ失敗するのか

echo コマンドは正常に実行されるとERRORLEVELを0にします。そのため、somecommand.exe が失敗しても、直後の echo によってERRORLEVELが0に上書きされ、エラー判定が常にスキップされます。

注意:echo以外にも、set、title、rem なども ERRORLEVELを変更する可能性があります。判定前には余計なコマンドを一切挟まないのが安全です。

OKコード(修正版)

OK: 判定を先に行い、その後にログ出力
@echo off

somecommand.exe
if %ERRORLEVEL% neq 0 (
    echo [ERROR] somecommand に失敗しました (code: %ERRORLEVEL%)
    exit /b %ERRORLEVEL%
)
echo 処理が完了しました

ポイント:ERRORLEVELを変数に退避する方法もあります。set RESULT=%ERRORLEVEL% として保存すれば後からいつでも参照可能です。ただし set 自体がERRORLEVELに影響する場合もあるため、コマンド直後に行いましょう。

失敗例3: if ERRORLEVELを等号比較だと思い込んでいる

if ERRORLEVEL N の構文は、多くの初心者が「ERRORLEVELがNと等しいかどうか」を判定するものだと誤解しています。しかし実際には「ERRORLEVELがN以上かどうか」を判定する構文です。

NGコード

NG: if errorlevel を等号比較と誤解
@echo off

somecommand.exe

if errorlevel 1 echo エラー発生
if errorlevel 0 echo 正常終了

なぜ失敗するのか

if errorlevel 0 は「ERRORLEVELが0以上」という意味なので、ERRORLEVELが1のときでもTRUEになります。つまり、ERRORLEVELが1の場合は「エラー発生」と「正常終了」の両方が実行されてしまいます。

実行結果(ERRORLEVELが1のとき)

エラー発生
正常終了

「エラー発生」のあとに「正常終了」も表示されてしまい、判定が破綻しています。

OKコード(修正版)

OK: %ERRORLEVEL% で等号比較する
@echo off

somecommand.exe

rem 方法1: %ERRORLEVEL% で等号比較
if %ERRORLEVEL% equ 0 (
    echo 正常終了
) else (
    echo エラー発生 (code: %ERRORLEVEL%)
)

rem 方法2: if errorlevel を使うなら降順で判定
if errorlevel 2 echo 致命的エラー
if errorlevel 1 echo 軽微なエラー
if errorlevel 0 echo 正常終了
構文 比較方式 推奨度
if errorlevel N N以上かどうか 降順で使えばOK
if %ERRORLEVEL% equ N Nと等しいか 推奨
if %ERRORLEVEL% neq 0 0以外か 最も汎用的

失敗例4: 括弧ブロック内で%ERRORLEVEL%を参照している

バッチファイルで最もハマりやすい落とし穴の一つが、括弧ブロック(複合文)内での変数展開です。%変数%は括弧ブロック全体が読み込まれた時点で展開されるため、実行時の値が反映されません。

NGコード

NG: 括弧ブロック内で%ERRORLEVEL%を参照
@echo off

if exist input.txt (
    somecommand.exe input.txt
    echo ERRORLEVEL = %ERRORLEVEL%
    if %ERRORLEVEL% neq 0 (
        echo エラーが発生しました
    )
)

なぜ失敗するのか

cmd.exeは括弧ブロック全体を一度にパースします。その際に %ERRORLEVEL% が展開されるため、somecommand.exe の実行前の値(通常は0)に置き換えられます。結果として、somecommand.exe がエラーを返しても常に「ERRORLEVEL = 0」と表示されます。

これは「遅延展開」の問題として知られており、括弧ブロック内で変数の変化を正しく追跡するには、setlocal enabledelayedexpansion!変数! 構文を使う必要があります。

OKコード(修正版)

OK: 遅延展開を使う
@echo off
setlocal enabledelayedexpansion

if exist input.txt (
    somecommand.exe input.txt
    echo ERRORLEVEL = !ERRORLEVEL!
    if !ERRORLEVEL! neq 0 (
        echo エラーが発生しました
    )
)

遅延展開が必要になるケース

  • if / else の括弧ブロック内で変数を参照する場合
  • for ループ内で変数の値が変化する場合
  • 複合コマンド(&& や || で連結した行)の後半で参照する場合

注意:遅延展開を有効にすると、感嘆符(!)が特殊文字として扱われます。文字列中に ! を含むファイル名やデータを扱う場合は、^! でエスケープする必要があります。

失敗例5: パイプ処理の結果をそのままエラー判定している

パイプ(|)を使ったコマンドでは、ERRORLEVELはパイプの最後のコマンドの終了コードになります。前段のコマンドが失敗していても検知できません。

NGコード

NG: パイプの前段の失敗を見逃す
@echo off

rem type が失敗しても find の結果だけが評価される
type data.txt | find "ERROR" > nul
if errorlevel 1 (
    echo エラー文字列が見つかりませんでした
) else (
    echo エラー文字列が見つかりました
)

なぜ失敗するのか

data.txt が存在しない場合、type はエラーを返しますが、パイプで接続された find は空の入力を受け取り、「見つからなかった」として ERRORLEVEL 1 を返します。問題は、type のエラーなのか find で見つからなかったのかが区別できない点です。

OKコード(修正版)

OK: 前段の存在チェックを分離する
@echo off

rem まずファイルの存在を確認
if not exist data.txt (
    echo [ERROR] data.txt が見つかりません
    exit /b 1
)

rem ファイルが存在することを確認してから検索
find "ERROR" data.txt > nul 2>&1
if %ERRORLEVEL% equ 0 (
    echo エラー文字列が見つかりました
) else (
    echo エラー文字列は見つかりませんでした
)

ポイント:パイプを使う場合は、前段の処理を事前チェックで切り離すか、一時ファイルに出力してから次の処理を実行するのが安全です。また、リダイレクト(>)でも同様の問題は起きにくいですが、2>&1 で標準エラーもまとめる習慣を付けておくとデバッグが容易です。

失敗例6: CALLなしでサブルーチンを呼んでしまう

バッチファイル内のサブルーチン(ラベル)を CALL なしで呼び出すと、制御がサブルーチンから戻ってこないという問題が発生します。

NGコード

NG: CALLなしでラベルにジャンプ
@echo off

echo メイン処理開始
goto :DoWork

rem ここは実行されない
echo メイン処理に戻りました
if %ERRORLEVEL% neq 0 echo エラー
exit /b 0

:DoWork
echo サブ処理実行中...
somecommand.exe
exit /b %ERRORLEVEL%

なぜ失敗するのか

goto :DoWork は単にラベルにジャンプするだけで、戻り先を記録しません。そのため、サブルーチンの exit /b が実行されるとバッチファイル全体が終了し、「メイン処理に戻りました」以降のコードは一切実行されません。

一方、call :DoWork はサブルーチンの実行後に呼び出し元の次の行に戻ります。ERRORLEVELもサブルーチンの exit /b の値が引き継がれます。

OKコード(修正版)

OK: CALLでサブルーチンを呼び出す
@echo off

echo メイン処理開始
call :DoWork

rem CALLなので、ここに戻ってくる
echo メイン処理に戻りました
if %ERRORLEVEL% neq 0 (
    echo [ERROR] サブルーチンでエラーが発生
    exit /b 1
)
echo すべて正常
exit /b 0

:DoWork
echo サブ処理実行中...
somecommand.exe
exit /b %ERRORLEVEL%
方法 戻ってくるか ERRORLEVELの引継
goto :label 戻ってこない N/A
call :label exit /b で戻る exit /b の値が反映

失敗例7: exit と exit /b を混同してしまう

exitexit /b はまったく異なる動作をします。この違いを理解していないと、エラー処理でバッチファイルだけでなくコマンドプロンプト自体が閉じてしまうという事故が起きます。

NGコード

NG: exit でcmd.exeごと終了する
@echo off

somecommand.exe
if %ERRORLEVEL% neq 0 (
    echo エラーが発生しました
    exit 1
)

なぜ失敗するのか

exit(/b なし)はcmd.exe プロセス自体を終了させます。コマンドプロンプトから手動で実行した場合はウィンドウが閉じ、別のバッチファイルから呼び出された場合は呼び出し元も巻き込んで終了します。

タスクスケジューラやCI/CDパイプラインから実行している場合は、後続のジョブがすべてスキップされるなど、影響が広範囲に及ぶことがあります。

OKコード(修正版)

OK: exit /b でバッチファイルだけ終了
@echo off

somecommand.exe
if %ERRORLEVEL% neq 0 (
    echo エラーが発生しました
    exit /b 1  rem バッチファイルだけ終了し、呼び出し元に戻る
)
コマンド 動作 影響範囲
exit cmd.exeプロセスを終了 ウィンドウが閉じる。呼出元も終了
exit /b 現在のバッチだけ終了 呼出元に制御が戻る
exit /b N 終了コードNで終了 ERRORLEVELがNに設定される

失敗例8: ファイルパスの空白に対応していない

ファイルパスに空白が含まれる場合、ダブルクォートで囲まないとパスが途中で分断され、意図しないエラーが発生します。

NGコード

NG: 空白を含むパスを引用符なしで使用
@echo off

set LOGDIR=C:\Program Files\MyApp\logs

rem 空白で分断される
if not exist %LOGDIR% (
    mkdir %LOGDIR%
)

rem copy先も分断される
copy output.log %LOGDIR%\output.log
if %ERRORLEVEL% neq 0 echo コピー失敗

なぜ失敗するのか

%LOGDIR% が展開されると C:\Program Files\MyApp\logs になります。引用符がないため、cmd.exe はこれを C:\ProgramFiles\MyApp\logs の2つの引数として解釈します。

結果として if not exist C:\Program という意図しない判定になり、mkdir も copy も失敗します。エラーメッセージも「構文が誤っています」のように不親切で、原因の特定が困難です。

OKコード(修正版)

OK: ダブルクォートでパスを囲む
@echo off

set "LOGDIR=C:\Program Files\MyApp\logs"

rem 変数参照時もダブルクォートで囲む
if not exist "%LOGDIR%" (
    mkdir "%LOGDIR%"
    if %ERRORLEVEL% neq 0 (
        echo [ERROR] ディレクトリ作成に失敗
        exit /b 1
    )
)

copy output.log "%LOGDIR%\output.log"
if %ERRORLEVEL% neq 0 echo [ERROR] コピーに失敗

ポイント:set 文でも set "VAR=値" と全体をダブルクォートで囲む書き方が推奨されます。set VAR="値" と書くと、値にダブルクォートが含まれてしまうので注意してください。

失敗例9: 特殊文字のエスケープ忘れ

バッチファイルでは &|<>^% などが特殊な意味を持ちます。これらをデータとして扱う際にエスケープを忘れると、予期しないエラーや動作になります。

NGコード

NG: & が特殊文字として解釈される
@echo off

rem 変数に&が含まれるとコマンド区切りと誤解される
set MSG=処理A&処理B完了
echo %MSG%

rem ファイル名に特殊文字が含まれる場合
copy report(2024).txt backup.txt
if %ERRORLEVEL% neq 0 echo 失敗

なぜ失敗するのか

set MSG=処理A&処理B完了 は、set MSG=処理A処理B完了 の2つのコマンドとして解釈されます。& はコマンドの区切り文字だからです。

同様に、括弧 () もバッチファイルでは特殊な意味(ブロック構文)を持つため、ファイル名に含まれていると構文エラーの原因になります。

OKコード(修正版)

OK: エスケープまたはダブルクォートで対処
@echo off

rem 方法1: ダブルクォートで囲む
set "MSG=処理A&処理B完了"
echo %MSG%

rem 方法2: キャレット(^)でエスケープ
echo 処理A^&処理B完了

rem ファイル名に特殊文字がある場合はダブルクォート
copy "report(2024).txt" backup.txt
if %ERRORLEVEL% neq 0 echo 失敗
特殊文字 意味 エスケープ方法
& コマンド区切り ^& またはダブルクォートで囲む
| パイプ ^|
< > リダイレクト ^< ^>
^ エスケープ文字 ^^
% 変数展開 %%(バッチ内)
! 遅延展開(有効時) ^!

失敗例10: robocopyの終了コードを0/非0で判定している

robocopy は他のコマンドと異なり、0以外の終了コードでも正常動作を意味します。これを知らずに「0以外はエラー」として処理すると、正常なファイルコピーをエラー扱いしてしまいます。

NGコード

NG: robocopyの終了コードを単純判定
@echo off

robocopy C:\source C:\dest /MIR

rem robocopyは正常でも1〜7を返すため、常にエラー扱いになる
if %ERRORLEVEL% neq 0 (
    echo [ERROR] robocopy に失敗しました
    exit /b 1
)
echo 正常に完了しました

なぜ失敗するのか

robocopy の終了コードはビットフラグで構成されており、0〜7は正常動作を示します。例えば「1」はファイルがコピーされたことを意味し、「0」は変更なし(コピー不要)を意味します。8以上が本当のエラーです。

終了コード 意味 判定
0 変更なし(コピー不要) 正常
1 ファイルがコピーされた 正常
2 余分なファイルが検出された 正常
4 不一致ファイルが検出された 警告
8 コピー失敗(リトライ超過) エラー
16 致命的エラー エラー

OKコード(修正版)

OK: robocopy専用のエラー判定
@echo off

robocopy C:\source C:\dest /MIR
set RC=%ERRORLEVEL%

rem 8以上がエラー(ビットフラグ)
if %RC% geq 8 (
    echo [ERROR] robocopy に失敗しました (code: %RC%)
    exit /b 1
)

rem 0〜7は正常(情報・警告含む)
echo robocopy 正常完了 (code: %RC%)
exit /b 0

ポイント:robocopy以外にも、xcopy(終了コード 0=正常, 4=コピー先の容量不足)やdsquery、net useなど、コマンドごとに終了コードの意味は異なります。エラー判定を書く前に、そのコマンドの終了コード仕様を確認する習慣をつけましょう。

失敗例11: findstr を単純なエラー判定に使っている

find や findstr は「文字列が見つからなかった」だけで ERRORLEVEL 1 を返します。これは処理の失敗ではなく、検索結果が0件であることを示しています。

NGコード

NG: findstr の不一致をエラーと誤解
@echo off

findstr "SUCCESS" result.log > nul
if %ERRORLEVEL% neq 0 (
    echo [ERROR] 処理に失敗しました
    exit /b 1
)

なぜ失敗するのか

findstr の ERRORLEVEL 1 は「一致する行が見つからなかった」を意味し、2が「ファイルが見つからなかった・構文エラー」です。上記コードでは、ファイルが存在してもSUCCESSの文字列がなければ「処理に失敗」と報告してしまいます。本当にやりたいのは「ログファイルの読み取りに失敗した」ケースの検出だけかもしれません。

OKコード(修正版)

OK: findstr の終了コードを正しく分岐
@echo off

rem まずファイルの存在を確認
if not exist result.log (
    echo [ERROR] result.log が見つかりません
    exit /b 2
)

findstr "SUCCESS" result.log > nul 2>&1
if %ERRORLEVEL% equ 0 (
    echo 処理は正常に完了しました
) else if %ERRORLEVEL% equ 1 (
    echo [WARNING] SUCCESS文字列が見つかりません。処理結果を確認してください
) else (
    echo [ERROR] findstr でエラーが発生しました
    exit /b 1
)
ERRORLEVEL find / findstr 適切な対応
0 一致する行が見つかった 正常として処理
1 一致する行が見つからなかった 業務ロジックに応じて判断
2 ファイルが見つからない・構文エラー エラーとして処理

失敗例12: サブルーチンの戻り値を設計していない

call で呼び出したサブルーチンで exit /b を明示しないと、ERRORLEVELが不定になり、呼び出し元のエラー判定が信頼できなくなります。

NGコード

NG: exit /b を書いていない
@echo off

call :Validate input.txt
if %ERRORLEVEL% neq 0 (
    echo バリデーション失敗
    exit /b 1
)
echo バリデーション成功
exit /b 0

:Validate
if not exist %1 (
    echo ファイルが見つかりません
    rem exit /b を忘れている!
)
echo ファイルを確認しました

なぜ失敗するのか

サブルーチン :Validate ではファイルが見つからない場合にエラーメッセージを表示しますが、exit /b 1 で終了コードを返していません。そのため、echoが成功してERRORLEVEL=0となり、呼び出し元では常に「バリデーション成功」と判定されます。

OKコード(修正版)

OK: 明示的に exit /b で終了コードを返す
@echo off

call :Validate input.txt
if %ERRORLEVEL% neq 0 (
    echo バリデーション失敗
    exit /b 1
)
echo バリデーション成功
exit /b 0

:Validate
if not exist "%~1" (
    echo [ERROR] %~1 が見つかりません
    exit /b 1
)
echo [OK] %~1 を確認しました
exit /b 0

ポイント:サブルーチンを設計するときは、すべての分岐で exit /b 終了コード を明示しましょう。正常時は exit /b 0、エラー時は exit /b 1 以上の値を返します。引数には %~1(チルダ付き)を使うと、渡されたダブルクォートが自動的に除去されます。

失敗例13: SET /A のエラー処理を怠る

SET /A は算術演算を行うコマンドですが、構文エラーがあってもERRORLEVELが0のままという罠があります。

NGコード

NG: SET /A のエラーを検出できない
@echo off

set INPUT=abc

rem 数値でない文字列を計算しようとする
set /a RESULT=%INPUT% + 10

rem ERRORLEVEL は 0 のままなのでエラーを検出できない
if %ERRORLEVEL% neq 0 (
    echo 計算エラー
) else (
    echo 計算結果: %RESULT%
)

実行結果

計算結果: 10

なぜ失敗するのか

SET /A は数値以外の文字列を0として扱うため、「abc + 10」は「0 + 10 = 10」と計算されてしまいます。ERRORLEVELも0のまま変化しないため、入力値の検証なしにはエラーを検出できません。

ゼロ除算の場合は標準エラーにメッセージが出ますが、それでもERRORLEVELは0のままです。

OKコード(修正版)

OK: 入力値を事前に検証する
@echo off

set INPUT=abc

rem 数値かどうかを事前にチェック
echo %INPUT% | findstr /r "^[0-9][0-9]*$" > nul
if %ERRORLEVEL% neq 0 (
    echo [ERROR] 入力値が数値ではありません: %INPUT%
    exit /b 1
)

set /a RESULT=%INPUT% + 10
echo 計算結果: %RESULT%

注意:SET /A はゼロ除算(set /a RESULT=10/0)でも ERRORLEVELを変更しません。標準エラーにメッセージが出力されるだけです。ゼロ除算を検出するには、除数が0でないことを事前にチェックする必要があります。

失敗例14: FORループ内のエラー処理が機能しない

FORループ内でのエラー処理は、失敗例4(遅延展開)と失敗例1(まとめて判定)の複合パターンです。ループ内でERRORLEVELを正しく取得するには、遅延展開が必須です。

NGコード

NG: FORループ内で%ERRORLEVEL%を参照
@echo off

set ERRORS=0

for %%f in (file1.txt file2.txt file3.txt) do (
    copy %%f backup\%%f
    if %ERRORLEVEL% neq 0 (
        echo [ERROR] %%f のコピーに失敗
        set /a ERRORS+=1
    )
)

echo エラー件数: %ERRORS%

なぜ失敗するのか

FORループの括弧ブロックも、if文の括弧ブロックと同様にパース時に%変数%が展開されます。そのため、ループ開始前のERRORLEVEL値(通常0)がすべてのイテレーションで使われ、ループ内のcopyの結果が反映されません。

同様に %ERRORS% もパース時に展開されるため、最終結果は常に 0(初期値)になります。

OKコード(修正版)

OK: 遅延展開で実行時の値を取得
@echo off
setlocal enabledelayedexpansion

set ERRORS=0

for %%f in (file1.txt file2.txt file3.txt) do (
    copy %%f backup\%%f > nul 2>&1
    if !ERRORLEVEL! neq 0 (
        echo [ERROR] %%f のコピーに失敗
        set /a ERRORS+=1
    ) else (
        echo [OK] %%f をコピーしました
    )
)

echo エラー件数: !ERRORS!
if !ERRORS! gtr 0 exit /b 1
exit /b 0

ポイント:FORループ内では %変数% ではなく !変数! を使うのが鉄則です。ERRORLEVEL、ループカウンタ、フラグ変数など、ループ内で値が変化するすべての変数に !…! を使いましょう。

失敗例15: バッチ連続実行時のエラー伝搬を考慮していない

複数のバッチファイルを連続して呼び出す場合、前のバッチのERRORLEVELが次のバッチに引き継がれることがあります。これを考慮しないと、前回のエラーが残っていたために誤判定するケースが発生します。

NGコード

NG: 前回のERRORLEVELが残る(呼び出し元)
@echo off

rem step1.bat がエラーで終了
call step1.bat

rem step2.bat 内でERRORLEVELをチェックすると
rem step1のエラーコードが残っている可能性がある
call step2.bat
NG: ERRORLEVELリセットなし(step2.bat)
@echo off

rem 最初のコマンド実行前に判定すると
rem 前回のERRORLEVELが残っている
echo 処理開始 (ERRORLEVEL=%ERRORLEVEL%)

rem この時点で ERRORLEVEL が 0 でない可能性がある
somecommand.exe

なぜ失敗するのか

CALLで呼ばれたバッチファイルの exit /b の値は、呼び出し元のERRORLEVELとして残ります。次のバッチファイルが、自身のコマンドを実行する前にERRORLEVELを参照すると、前回の値が見えてしまいます。

OKコード(修正版)

OK: 各ステップでエラー判定と伝搬制御
@echo off

echo === Step 1 ===
call step1.bat
if %ERRORLEVEL% neq 0 (
    echo [ERROR] step1 失敗。処理を中断します。
    exit /b 1
)

echo === Step 2 ===
call step2.bat
if %ERRORLEVEL% neq 0 (
    echo [ERROR] step2 失敗。処理を中断します。
    exit /b 1
)

echo すべてのステップが正常に完了しました
exit /b 0
OK: 呼ばれる側でもERRORLEVELを明確に
@echo off

rem ERRORLEVELを明示的にリセット
cmd /c "exit /b 0"

rem ここからが本来の処理
somecommand.exe
if %ERRORLEVEL% neq 0 (
    echo [ERROR] 処理に失敗しました
    exit /b 1
)
exit /b 0

ポイント:ERRORLEVELをリセットする方法はいくつかあります。cmd /c "exit /b 0"ver > nul(ver は常に0を返す)、(call )(空のcall)などが使えます。バッチの先頭でリセットする習慣を付けると安全です。

失敗パターン早見表

15のパターンを一覧で振り返ります。エラー処理を書くときのチェックリストとして活用してください。

No. 失敗パターン 対策
1 まとめて判定 各コマンド直後に判定する
2 判定前に別コマンド echoなどを挟まない
3 if ERRORLEVELの誤解 %ERRORLEVEL% equ/neq を使う
4 括弧内で%変数% 遅延展開 + !変数! を使う
5 パイプの結果判定 前段を事前チェックで分離
6 CALLなし呼び出し call :label で呼び出す
7 exit と exit /b の混同 常に exit /b を使う
8 空白パスの未対応 ダブルクォートで囲む
9 特殊文字の未エスケープ ^でエスケープ or ダブルクォート
10 robocopyの誤判定 8以上をエラーとする
11 findstrの誤判定 0/1/2を個別に判定する
12 戻り値の未設計 全分岐で exit /b N を明示
13 SET /A のエラー 入力値を事前検証する
14 FORループ内の判定 遅延展開 + !ERRORLEVEL!
15 バッチ連続実行の伝搬 各バッチ先頭でリセット

エラー処理を正しく書くための5つの原則

15の失敗例を通じて見えてくる、バッチファイルのエラー処理における共通原則をまとめます。

5つの原則

  • 原則1: 即座に判定する ── コマンド実行直後にERRORLEVELをチェック。間に何も挟まない
  • 原則2: 遅延展開を理解する ── 括弧ブロック・FORループ内では !ERRORLEVEL! を使う
  • 原則3: コマンドの仕様を確認する ── robocopy、findstr、xcopy など、コマンドごとに終了コードの意味が異なる
  • 原則4: 戻り値を設計する ── サブルーチンはすべての分岐で exit /b N を明示する
  • 原則5: 防御的に書く ── パスはダブルクォートで囲み、特殊文字はエスケープし、入力値を検証する

よくある質問(FAQ)

Q. バッチファイルのエラーをデバッグするときの基本的な方法は何ですか?
A. echo onでコマンドの実行内容を表示するか、pauseを要所に挿入して処理を止めて確認します。またecho %変数名%で変数の中身を確認するのも有効です。エラーが発生したらif %errorlevel% neq 0でエラーチェックポイントを設けると原因が特定しやすくなります。
Q. バッチファイルでファイルパスにスペースが含まれているとエラーになります。なぜですか?
A. スペースを含むパスはコマンドの引数区切りとして誤解釈されます。ダブルクォートで囲むことで解決できます:"C:\Program Files\myapp.exe"。変数を使う場合も"%PATH_VAR%"のようにクォートで囲みます。
Q. バッチファイルを実行すると一瞬だけウィンドウが開いて閉じてしまいます。内容を確認するにはどうすればよいですか?
A. バッチファイルの末尾にpauseコマンドを追加することで、「続行するには何かキーを押してください…」と表示されて止まります。または右クリック→「管理者として実行」ではなく、コマンドプロンプトを開いてからバッチファイルを実行する方法もあります。

まとめ

バッチファイルのエラー処理で失敗する多くの原因は、ERRORLEVELの仕様を前提にせず「感覚的」に判定していることにあります。

エラー判定は、どのコマンドの結果を見ているのかその終了コードは何を意味するのかを明確にしたうえで、直後に評価する設計が必要です。

特に遅延展開(setlocal enabledelayedexpansion + !変数!)は、括弧ブロックやFORループを使うバッチファイルでは避けて通れない知識です。本記事で紹介した15のパターンを把握しておけば、ERRORLEVELを「信用できない存在」ではなく「正しく使える判定材料」として活用できるようになります。

エラー処理は地味な作業ですが、バッチファイルの信頼性を大きく左右します。一度正しいパターンを身につければ、以後のトラブルを大幅に減らせるはずです。

バッチファイルのエラーハンドリング一覧

目的や状況別に、エラー処理の方法を整理しています。