【bat】ERRORLEVELが常に1になる12の原因と修正方法 ── チェックリスト形式で完全解説

【bat】バッチファイルでERRORLEVELが常に1になるときのチェックリスト bat
TypeScriptのユーティリティ型を全20種以上、実務ユースケース付きで徹底解説。Partial・Required・Readonly・Record・Pick・Omit・Exclude・Extract・NonNullable・ReturnType・Parameters・Awaited・文字列操作型・カスタムユーティリティ型の作り方まで網羅。

バッチファイルを書いていると、どのコマンドを実行しても ERRORLEVEL が常に 1 のまま変わらない、という状況に遭遇することがあります。

正常系のコマンドを実行しても 1、エラーが起きそうもない単純な処理でも 1。こうなると、ERRORLEVEL を使ったエラーハンドリングが完全に機能しなくなり、バッチ全体の信頼性が崩壊します。

しかし、この問題の原因は多くの場合「本当にエラーが起きている」のではなく、ERRORLEVEL の参照方法・更新タイミング・コマンド固有の仕様のいずれかを誤解していることにあります。

本記事では、ERRORLEVEL が常に 1 になってしまう12の主要な原因を、実務での遭遇頻度が高い順にチェックリスト形式で整理します。各項目には原因の解説・再現コード・修正方法を完備していますので、上から順番に確認していくことで確実に問題を特定できます。

この記事で学べること

  • ERRORLEVEL が常に 1 になる12の原因と、それぞれの修正方法
  • set ERRORLEVEL= による環境変数の上書きトラップ
  • 括弧ブロック内での %ERRORLEVEL% 展開タイミング問題と遅延展開
  • find / findstr / robocopy などコマンド固有の終了コード仕様
  • パイプ・リダイレクト・call / start が ERRORLEVEL に与える影響
  • 原因を体系的に切り分ける診断スクリプトの書き方
  • 実務で使える防御的コーディングパターン
No. チェック項目 原因カテゴリ 遭遇頻度
1 コマンド自体が失敗扱いの終了コードを返している コマンド仕様 ★★★
2 ERRORLEVEL を更新しないコマンドを挟んでいる 更新タイミング ★★★
3 判定対象コマンドとの距離が空いている 更新タイミング ★★★
4 パイプの後段コマンドが原因になっている パイプ仕様 ★★☆
5 if ERRORLEVEL の「以上比較」を誤解している 構文仕様 ★★☆
6 括弧ブロック内で %ERRORLEVEL% が展開済みになっている 遅延展開 ★★★
7 robocopy 等の特殊な終了コードを誤判定している コマンド仕様 ★★☆
8 set ERRORLEVEL= で環境変数を上書きしている 変数トラップ ★★☆
9 サブルーチン / call の戻り値が制御されていない 戻り値設計 ★★☆
10 call / start の ERRORLEVEL 伝播仕様を誤解している call/start仕様 ★☆☆
11 リダイレクトのエラーが ERRORLEVEL に影響している リダイレクト ★☆☆
12 FORループ内でERRORLEVELが固定されている 遅延展開 ★★☆

ERRORLEVELの基本から確認したい場合は、ERRORLEVELを使ってエラーハンドリングを行う方法も参考にしてください。

スポンサーリンク
  1. チェック1:コマンド自体が失敗扱いの終了コードを返していないか
    1. find / findstr の終了コード
    2. 問題が起きるコード
    3. 修正方法
    4. fc(ファイル比較)の終了コード
    5. 主要コマンドの終了コード一覧
  2. チェック2:ERRORLEVELを更新しないコマンドを挟んでいないか
    1. 問題が起きるコード
    2. 修正方法
    3. ERRORLEVELを更新するコマンド vs しないコマンドの見分け方
  3. チェック3:判定対象のコマンドと距離が空いていないか
    1. 問題が起きるコード
    2. 修正方法:判定は対象コマンドの直後に行う
    3. || 演算子を使った代替パターン
  4. チェック4:パイプ処理の最後のコマンドが原因になっていないか
    1. 問題が起きるコード
    2. 修正方法:パイプを分解して個別に判定する
    3. パイプと ERRORLEVEL の詳細な挙動
  5. チェック5:if ERRORLEVEL の「以上比較」を誤解していないか
    1. if ERRORLEVEL の挙動を正確に理解する
    2. 問題が起きるコード
    3. 逆に「常に 1」に見えるパターン
    4. 修正方法:等号比較を使う
  6. チェック6:括弧ブロック内で %ERRORLEVEL% が展開済みになっていないか
    1. 問題が起きるコード
    2. 修正方法:遅延環境変数展開を使う
    3. % と ! の違いを理解する
    4. 遅延展開が使えない場合の代替手段
  7. チェック7:robocopy など特殊な終了コードを返すコマンドを使っていないか
    1. robocopy の終了コード体系
    2. 問題が起きるコード
    3. 修正方法:robocopy 専用の判定ロジック
  8. チェック8:set ERRORLEVEL= で環境変数を上書きしていないか
    1. なぜこの問題が起きるのか
    2. 問題が起きるコード
    3. 修正方法:ユーザー変数 ERRORLEVEL を削除する
    4. この問題を検出する方法
  9. チェック9:サブルーチン / call の戻り値が制御されていないか
    1. 問題が起きるコード
    2. 修正方法:exit /b で戻り値を明示する
  10. チェック10:call / start の ERRORLEVEL 伝播仕様を誤解していないか
    1. call 時に ERRORLEVEL が常に 1 になるケース
    2. 修正方法:呼び出し前にファイル存在確認を行う
  11. チェック11:リダイレクトのエラーが ERRORLEVEL に影響していないか
    1. 問題が起きるコード
    2. 修正方法:リダイレクト先の存在確認と権限チェック
  12. チェック12:FORループ内で ERRORLEVEL が固定されていないか
    1. 問題が起きるコード
    2. 修正方法:遅延展開を使う
  13. 原因を体系的に切り分ける ── 診断スクリプト
  14. 実務で使える防御的コーディングパターン
    1. パターン1:コマンド実行直後に即座に判定する
    2. パターン2:|| 演算子で失敗時の即時ジャンプ
    3. パターン3:遅延展開+エラーカウントの統合テンプレート
    4. パターン4:robocopy 対応テンプレート
    5. パターン5:エラーログ付きの堅牢なテンプレート
  15. どうしても切り分けられないときの最終手段
    1. 手順1:ERRORLEVEL を強制的に 0 にリセットする
    2. 手順2:新しいcmd.exeで確認
    3. 手順3:バッチファイルの文字コード・改行コードを確認
  16. チェックリスト早見表
  17. よくある質問(FAQ)
  18. まとめ
  19. ERRORLEVEL関連記事

チェック1:コマンド自体が失敗扱いの終了コードを返していないか

ERRORLEVEL が常に 1 になっているとき、最初に疑うべきは実行しているコマンドが本当に成功しているのかという点です。

多くの開発者が見落とすのが、find / findstr / fc などの検索・比較系コマンドの仕様です。これらのコマンドは、処理自体はエラーなく完了していても、検索条件に一致するものが見つからなかっただけで ERRORLEVEL を 1 に設定します。

find / findstr の終了コード

find コマンドと findstr コマンドは、検索結果の有無で以下のように終了コードを返します。

ERRORLEVEL find の意味 findstr の意味
0 検索文字列が見つかった パターンに一致する行が見つかった
1 検索文字列が見つからなかった パターンに一致する行がなかった
2 ファイルが見つからない / 構文エラー ファイルが見つからない / 構文エラー

問題が起きるコード

以下のコードは、ファイル内に「ERROR」という文字列がなければ ERRORLEVEL が 1 になります。

問題のあるコード ─ find の戻り値を考慮していない
@echo off

rem ログファイルにERRORがあるか検索
find "ERROR" app.log > nul

rem ここでERRORLEVELを確認
echo 結果: %ERRORLEVEL%

rem 「ERROR」がファイルに含まれていなければ ERRORLEVEL = 1
rem これは「エラー」ではなく「見つからなかった」という正常な結果

実行結果(app.logにERRORが含まれていない場合)

結果: 1

修正方法

find / findstr の結果で後続処理を分岐する場合は、ERRORLEVEL の意味を正しく理解したうえで判定します。

修正後 ─ find の戻り値を正しく解釈する
@echo off

find "ERROR" app.log > nul 2>&1

if %ERRORLEVEL% equ 0 (
    echo ERROR が見つかりました。ログを確認してください。
) else if %ERRORLEVEL% equ 1 (
    echo ERROR は見つかりませんでした。正常です。
) else (
    echo ファイルが見つからないか、構文エラーです。
)

fc(ファイル比較)の終了コード

fc コマンドも同様に、2つのファイルの内容が異なるだけで ERRORLEVEL を 1 に設定します。

ERRORLEVEL fc の意味
0 2つのファイルの内容が一致
1 2つのファイルの内容が異なる
2 ファイルが見つからない

ポイント:検索・比較系コマンドの ERRORLEVEL 1 は「エラー」ではなく「条件不一致」です。これらのコマンドの後で ERRORLEVEL を見るときは、コマンドの仕様書を確認し、各終了コードの意味を把握してから判定ロジックを書きましょう。

主要コマンドの終了コード一覧

バッチファイルでよく使うコマンドの終了コードを以下にまとめます。「0以外=エラー」ではないコマンドが意外に多いことに注目してください。

コマンド 0 1 2以上 備考
find 一致あり 一致なし 構文/ファイルエラー 1は正常動作
findstr 一致あり 一致なし 構文/ファイルエラー 1は正常動作
fc 一致 差異あり ファイルなし 1は正常動作
robocopy 変更なし コピー成功 ビットフラグ 8以上がエラー
xcopy 成功 コピー0件 Ctrl+C/エラー 1は注意が必要
copy 成功 失敗 シンプルな0/1
ping 応答あり 応答なし 1はタイムアウト
net use 成功 各種エラー 2=ファイルなし等

チェック2:ERRORLEVELを更新しないコマンドを挟んでいないか

ERRORLEVEL は、すべてのコマンドが更新するわけではありません。ERRORLEVEL を変更しないコマンドが存在し、これを知らないと「前のコマンドのエラーが消えていない」ように見えます。

具体的には、以下のコマンドは ERRORLEVEL を変更しません。

コマンド ERRORLEVEL を更新するか 説明
echo 更新しない メッセージ表示のみ
set(値の代入) 更新しない 変数への値セットのみ
rem 更新しない コメント行
title 更新しない ウィンドウタイトル変更
color 更新しない コンソール色変更
cls 更新しない 画面クリア
if / else 更新しない 条件分岐の構文自体
goto 更新しない ラベルジャンプ

問題が起きるコード

問題 ─ echo は ERRORLEVEL を更新しない
@echo off

rem find が一致なしで ERRORLEVEL = 1 を返す
find "NOTEXIST" data.txt > nul 2>&1

rem echo は ERRORLEVEL を更新しない → 1 のまま残る
echo 処理を続行します...

rem set も ERRORLEVEL を更新しない → まだ 1 のまま
set RESULT=OK

rem ここで確認すると、find の ERRORLEVEL = 1 がまだ残っている
echo ERRORLEVEL = %ERRORLEVEL%

実行結果

処理を続行します...
ERRORLEVEL = 1

echoset を実行しても、前の find コマンドが返した ERRORLEVEL = 1 はリセットされません。これが「ERRORLEVEL がずっと 1 のまま」に見える典型的な原因です。

修正方法

ERRORLEVEL をリセットしたい場合は、明示的に 0 を設定するコマンドを実行します。

修正 ─ ERRORLEVEL を明示的にリセットする
@echo off

find "NOTEXIST" data.txt > nul 2>&1
echo find の結果: %ERRORLEVEL%

rem ERRORLEVEL を 0 にリセット
cmd /c exit /b 0

rem これで ERRORLEVEL = 0 からのリスタートになる
echo リセット後: %ERRORLEVEL%

実行結果

find の結果: 1
リセット後: 0

注意:cmd /c exit /b 0 は ERRORLEVEL をリセットする定番テクニックです。ver > nul でも 0 にリセットできますが、cmd /c exit /b N なら任意の値を設定できるため汎用性が高くおすすめです。

ERRORLEVELを更新するコマンド vs しないコマンドの見分け方

残念ながら、どのコマンドが ERRORLEVEL を更新し、どのコマンドが更新しないかについて、公式ドキュメントに統一的な一覧はありません。実務では以下の基準で判断します。

判断基準

  • 外部コマンド(.exe / .com)→ 基本的に ERRORLEVEL を更新する
  • 内部コマンドで処理を実行するもの(copy, move, del, mkdir 等)→ 更新する
  • 内部コマンドで表示・設定のみのもの(echo, set, rem, title 等)→ 更新しない
  • 不確かな場合は echo %ERRORLEVEL% で実際に確認する

チェック3:判定対象のコマンドと距離が空いていないか

ERRORLEVEL は「直前に実行されたコマンド」の終了コードです。ERRORLEVEL を更新するコマンドが対象コマンドと判定文の間に挟まると、まったく別のコマンドの結果を判定してしまいます。

問題が起きるコード

問題 ─ 判定対象とERRORLEVEL参照の間にコマンドがある
@echo off

rem 存在しないファイルをコピー → ERRORLEVEL = 1
copy nonexist.txt dest.txt > nul 2>&1

rem ログ出力のつもりで dir を実行
dir C:\ > nul

rem ここで判定 → copy ではなく dir の ERRORLEVEL を見ている!
if %ERRORLEVEL% neq 0 (
    echo コピーに失敗しました
) else (
    echo コピーに成功しました
)

実行結果

コピーに成功しました

copy は失敗しているのに、間に挟まった dir C:\(常に成功)が ERRORLEVEL を 0 に上書きしてしまい、エラーを検出できていません。

逆のパターンとして、成功したコマンドの後に失敗するコマンドが挟まると、「常に 1 になる」ように見えます。

修正方法:判定は対象コマンドの直後に行う

修正後 ─ コマンド直後に判定する
@echo off

copy nonexist.txt dest.txt > nul 2>&1
if %ERRORLEVEL% neq 0 (
    echo コピーに失敗しました(ERRORLEVEL=%ERRORLEVEL%)
    goto :ERROR
)

rem コピー成功後の処理
dir C:\ > nul
echo コピーに成功しました

|| 演算子を使った代替パターン

||(条件付き実行演算子)を使えば、コマンドの成否を直後に判定する構文がより簡潔に書けます。

代替 ─ || 演算子で直後に判定
@echo off

rem コマンド失敗時に即座にエラー処理へ
copy nonexist.txt dest.txt > nul 2>&1 || (
    echo コピーに失敗しました
    goto :ERROR
)

rem 成功した場合のみここに到達
echo コピーに成功しました

ポイント:|| を使えば ERRORLEVEL の上書きリスクを回避できます。ERRORLEVEL の明示的なチェックが不要な場面では、|| の方が安全で読みやすいコードになります。

チェック4:パイプ処理の最後のコマンドが原因になっていないか

パイプ(|)を使った処理では、ERRORLEVEL はパイプラインの最後のコマンドの終了コードになります。前段のコマンドが成功していても、後段のコマンドが条件不一致で 1 を返せば、ERRORLEVEL は 1 になります。

問題が起きるコード

問題 ─ パイプ後段の find が ERRORLEVEL を決定する
@echo off

rem type は成功するが、find が一致なしで 1 を返す
type sample.txt | find "ABC" > nul

rem ERRORLEVEL は find の結果 = 1
echo ERRORLEVEL = %ERRORLEVEL%

実行結果(sample.txtにABCが含まれていない場合)

ERRORLEVEL = 1

type コマンド自体は正常に動作していますが、パイプで渡された find コマンドが「見つからなかった」として 1 を返すため、全体として ERRORLEVEL = 1 になります。

修正方法:パイプを分解して個別に判定する

修正後 ─ パイプを分解して各段階で判定
@echo off

rem ステップ1: ファイル読み込み
type sample.txt > temp_output.txt 2>&1
if %ERRORLEVEL% neq 0 (
    echo ファイルの読み込みに失敗しました
    goto :CLEANUP
)

rem ステップ2: 文字列検索
find "ABC" temp_output.txt > nul
if %ERRORLEVEL% equ 0 (
    echo ABCが見つかりました
) else (
    echo ABCは見つかりませんでした(これはエラーではない)
)

:CLEANUP
if exist temp_output.txt del temp_output.txt

パイプと ERRORLEVEL の詳細な挙動

パイプの ERRORLEVEL に関して、もう少し踏み込んだ挙動を確認しておきましょう。

パイプ構文 前段の結果 後段の結果 ERRORLEVEL
echo OK | find "OK" 成功(0) 一致あり(0) 0
echo OK | find "NG" 成功(0) 一致なし(1) 1
type notexist | find "OK" 失敗(1) 一致なし(1) 1
dir | find /c "" 成功(0) 一致あり(0) 0

注意:パイプの前段コマンドが失敗した場合でも、ERRORLEVEL は後段コマンドの結果で上書きされます。つまり、前段のエラーがパイプによって「隠蔽」されることもあります。重要なコマンドの成否を判定する場合は、パイプを使わずに一時ファイルを経由するのが安全です。

チェック5:if ERRORLEVEL の「以上比較」を誤解していないか

バッチファイルの if ERRORLEVEL N 構文は、多くの開発者が「ERRORLEVEL が N と等しいかどうか」を判定すると誤解しています。実際には、ERRORLEVEL が N 以上かどうかを判定する「以上比較」です。

if ERRORLEVEL の挙動を正確に理解する

構文 意味 ERRORLEVEL=1 のとき
if ERRORLEVEL 0 ERRORLEVEL >= 0 ? TRUE(1 >= 0)
if ERRORLEVEL 1 ERRORLEVEL >= 1 ? TRUE(1 >= 1)
if ERRORLEVEL 2 ERRORLEVEL >= 2 ? FALSE(1 < 2)

問題が起きるコード

問題 ─ if ERRORLEVEL 0 は常に TRUE になる
@echo off

find "EXIST" data.txt > nul

rem この判定は ERRORLEVEL が 0 以上なら TRUE
rem → ERRORLEVEL は常に 0 以上なので、常に TRUE になる!
if ERRORLEVEL 0 (
    echo 正常終了
) else (
    echo エラー発生
)

このコードでは、if ERRORLEVEL 0 は「ERRORLEVEL が 0 以上か」を判定するため、ERRORLEVEL が 0 でも 1 でも 255 でも常に TRUE になります。結果として、エラーが発生しても「正常終了」と表示されてしまいます。

逆に「常に 1」に見えるパターン

問題 ─ if ERRORLEVEL 1 が TRUE のとき「常にエラー」に見える
@echo off

rem 何らかのコマンドを実行
somecommand

rem ERRORLEVEL が 1「以上」ならエラーメッセージ
if ERRORLEVEL 1 (
    echo ERRORLEVEL が 1 です
)

rem 仮に ERRORLEVEL = 2 でも「1 です」と表示される
rem 仮に ERRORLEVEL = 100 でも「1 です」と表示される

この構文では、ERRORLEVEL が 1 でも 2 でも 100 でも「ERRORLEVEL が 1 です」と表示されます。「常に 1 になっている」と勘違いする原因になります。

修正方法:等号比較を使う

修正後 ─ %ERRORLEVEL% による等号比較
@echo off

somecommand

rem 等号比較: ERRORLEVEL が正確に 0 かどうか
if %ERRORLEVEL% equ 0 (
    echo 正常終了
) else if %ERRORLEVEL% equ 1 (
    echo ERRORLEVEL = 1(条件不一致など)
) else (
    echo ERRORLEVEL = %ERRORLEVEL%(その他のエラー)
)
方法 比較タイプ 推奨度 注意点
if ERRORLEVEL N N以上 非推奨 誤解しやすい
if %ERRORLEVEL% equ N 等号 推奨 括弧内では遅延展開が必要
if %ERRORLEVEL% neq 0 不等号 推奨 0以外をエラーとする場合に便利
command || action 失敗時実行 推奨 最もシンプル

チェック6:括弧ブロック内で %ERRORLEVEL% が展開済みになっていないか

これは ERRORLEVEL が「常に 1」になる原因として最も多く、最も理解しにくい問題です。

バッチファイルの括弧ブロック(if (...)for (...) など)では、ブロック全体がパース(解析)される時点で %変数% が展開(値に置換)されます。つまり、ブロック内のコマンドが実行される前に、変数の値が確定してしまうのです。

問題が起きるコード

問題 ─ 括弧ブロック内で %ERRORLEVEL% がパース時に固定される
@echo off

rem この時点で ERRORLEVEL = 0

if 1==1 (
    rem ブロック全体がパースされる時点で
    rem %ERRORLEVEL% は 0 に展開される

    find "NOTEXIST" data.txt > nul
    rem 実行後の ERRORLEVEL は 1 だが...

    echo ERRORLEVEL = %ERRORLEVEL%
    rem パース時に 0 に置換済みなので 0 と表示される
)

実行結果

ERRORLEVEL = 0

find コマンドは失敗して ERRORLEVEL = 1 になっているはずなのに、echo %ERRORLEVEL% は 0 を表示します。これは、括弧ブロックの入り口(パース時)の ERRORLEVEL の値(0)がブロック全体に適用されるためです。

逆に、ブロックに入る前の ERRORLEVEL が 1 だった場合は、ブロック内でどんなコマンドを実行しても %ERRORLEVEL% は常に 1 と表示されます。これが「ERRORLEVEL が常に 1 になる」と感じる典型的なケースです。

修正方法:遅延環境変数展開を使う

setlocal enabledelayedexpansion を宣言し、変数を !変数名!(感嘆符)で参照することで、ブロック内でも実行時の最新値を取得できます。

修正後 ─ 遅延展開で実行時の値を取得
@echo off
setlocal enabledelayedexpansion

if 1==1 (
    find "NOTEXIST" data.txt > nul

    rem !ERRORLEVEL! なら実行時の値を参照できる
    echo ERRORLEVEL = !ERRORLEVEL!
)

実行結果

ERRORLEVEL = 1

% と ! の違いを理解する

参照方法 展開タイミング 括弧ブロック内 FORループ内
%ERRORLEVEL% パース時(行/ブロック読み込み時) 固定値になる 固定値になる
!ERRORLEVEL! 実行時(各コマンド実行直前) 最新値を取得 最新値を取得

遅延展開が使えない場合の代替手段

一部の状況(感嘆符がファイル名に含まれるケースなど)では遅延展開が使えないことがあります。その場合は、call コマンドによる「二段階展開」で回避できます。

代替 ─ call による二段階展開
@echo off

if 1==1 (
    find "NOTEXIST" data.txt > nul

    rem call を使うと %%ERRORLEVEL%% が実行時に展開される
    call echo ERRORLEVEL = %%ERRORLEVEL%%
)

実行結果

ERRORLEVEL = 1

遅延展開のまとめ

  • %ERRORLEVEL% は括弧ブロック内ではパース時の値に固定される
  • !ERRORLEVEL!実行時の最新値を取得する(setlocal enabledelayedexpansion が必要)
  • 遅延展開が使えない場合は call echo %%ERRORLEVEL%% で代用可能
  • 括弧ブロック外(通常の行)では %ERRORLEVEL% でも正しく動作する

チェック7:robocopy など特殊な終了コードを返すコマンドを使っていないか

Windows のファイルコピーコマンドである robocopy は、他のコマンドとは根本的に異なるビットフラグ方式の終了コードを返します。これを「0以外=エラー」と解釈すると、正常なコピー操作でも ERRORLEVEL が常に 1 以上になります。

robocopy の終了コード体系

ビット 意味 エラー?
0 0 コピーなし(変更なし) No
0 1 ファイルが正常にコピーされた No
1 2 余分なファイル/ディレクトリが検出された No
3 1 + 2(コピー成功 + 余分ファイル検出) No
2 4 不一致ファイル/ディレクトリが検出された 注意
3 8 コピー失敗(リトライ超過等) Yes
4 16 致命的エラー(パス不正等) Yes

注意:robocopy では ERRORLEVEL = 1 は「ファイルが正常にコピーされた」という成功を意味します。「0以外=エラー」の先入観で判定すると、正常動作なのにエラーとして処理してしまいます。

問題が起きるコード

問題 ─ robocopy の成功を失敗と判定してしまう
@echo off

robocopy C:\source C:\dest /MIR

rem robocopy の ERRORLEVEL = 1(コピー成功)を
rem エラーとして誤判定してしまう
if %ERRORLEVEL% neq 0 (
    echo エラーが発生しました!
    exit /b 1
)

修正方法:robocopy 専用の判定ロジック

修正後 ─ robocopy の終了コードを正しく判定
@echo off

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

rem robocopy では 8 以上がエラー
if %RC% geq 8 (
    echo robocopy でエラーが発生しました(終了コード: %RC%)
    exit /b 1
)

rem 0〜7 は正常(0=変更なし, 1=コピー成功, 2=余分ファイル検出...)
echo robocopy 正常終了(終了コード: %RC%)

rem 後続処理のために ERRORLEVEL を 0 にリセット
cmd /c exit /b 0

ポイント:robocopy 以外にも、dsquery(Active Directory 検索)や schtasks(タスクスケジューラ)など、独自の終了コード体系を持つコマンドがあります。初めて使うコマンドは、必ず公式ドキュメントで終了コードの意味を確認しましょう。

チェック8:set ERRORLEVEL= で環境変数を上書きしていないか

これは ERRORLEVEL に関する最も危険なトラップの一つです。set ERRORLEVEL=値 を実行すると、システムの ERRORLEVEL とは別の「ユーザー環境変数 ERRORLEVEL」が作成され、以降 %ERRORLEVEL% はそのユーザー変数の値を返すようになります。

なぜこの問題が起きるのか

バッチファイルで %ERRORLEVEL% を参照すると、cmd.exe は以下の優先順で値を探します。

優先度 参照先 説明
1 ユーザー環境変数 ERRORLEVEL set コマンドで作成された変数。存在すればこちらが優先
2 システムの ERRORLEVEL 直前のコマンドの終了コード。ユーザー変数がなければこちら

問題が起きるコード

問題 ─ set ERRORLEVEL= でシステム値が隠蔽される
@echo off

rem 意図せず ERRORLEVEL というユーザー変数を作成
set ERRORLEVEL=1

rem 成功するコマンドを実行
dir C:\ > nul

rem dir は成功(システムの ERRORLEVEL = 0)だが
rem ユーザー変数の ERRORLEVEL = 1 が優先される
echo ERRORLEVEL = %ERRORLEVEL%

実行結果

ERRORLEVEL = 1

dir C:\ は確実に成功するのに、%ERRORLEVEL% が 1 を返しています。これは、set ERRORLEVEL=1 で作成したユーザー環境変数が、システムの終了コードを隠蔽しているためです。

修正方法:ユーザー変数 ERRORLEVEL を削除する

修正後 ─ ユーザー変数を削除してシステム値を復元
@echo off

rem ユーザー変数 ERRORLEVEL を削除
rem (値なしで set すると変数が削除される)
set ERRORLEVEL=

rem これでシステムの ERRORLEVEL が参照されるようになる
dir C:\ > nul
echo ERRORLEVEL = %ERRORLEVEL%

実行結果

ERRORLEVEL = 0

この問題を検出する方法

ユーザー変数 ERRORLEVEL が存在するかどうかは、set コマンドで確認できます。

検出 ─ ユーザー変数 ERRORLEVEL の存在確認
rem ERRORLEVEL で始まる変数を一覧表示
set ERRORLEVEL

rem 出力例(ユーザー変数が存在する場合):
rem ERRORLEVEL=1

rem 出力例(ユーザー変数が存在しない場合):
rem 環境変数 ERRORLEVEL が定義されていません

注意:同じ問題は %CD%%DATE%%TIME%%RANDOM% などのシステム疑似変数でも発生します。これらの名前でユーザー変数を作成すると、システム値が隠蔽されます。絶対にこれらの名前で set を使わないことが鉄則です。

チェック9:サブルーチン / call の戻り値が制御されていないか

call コマンドでサブルーチンや別のバッチファイルを呼び出す場合、呼び出し先の最後に実行されたコマンドの ERRORLEVEL がそのまま呼び出し元に伝播します。

サブルーチン内で意図せず失敗するコマンドがあると、呼び出し元では「常に ERRORLEVEL が 1」の状態が続くことになります。

問題が起きるコード

問題 ─ サブルーチンの戻り値が制御されていない
@echo off

call :CHECK_FILE
echo 戻り後の ERRORLEVEL = %ERRORLEVEL%
goto :EOF

:CHECK_FILE
rem ファイルの存在チェック
if exist important.txt (
    echo ファイルが存在します
) else (
    echo ファイルが見つかりません
)

rem ログ検索(見つからなければ ERRORLEVEL = 1)
find "COMPLETED" process.log > nul 2>&1

rem exit /b を書いていないので、find の ERRORLEVEL がそのまま戻る
goto :EOF

実行結果(process.logにCOMPLETEDが含まれていない場合)

ファイルが存在します
戻り後の ERRORLEVEL = 1

サブルーチンの最後のコマンド find が 1 を返しているため、呼び出し元の ERRORLEVEL も 1 になっています。

修正方法:exit /b で戻り値を明示する

修正後 ─ サブルーチンで exit /b を使って戻り値を制御
@echo off

call :CHECK_FILE
if %ERRORLEVEL% equ 0 (
    echo チェック正常完了
) else (
    echo チェックでエラーが見つかりました
)
goto :EOF

:CHECK_FILE
if not exist important.txt (
    echo ファイルが見つかりません
    exit /b 1
)

find "COMPLETED" process.log > nul 2>&1
if %ERRORLEVEL% neq 0 (
    echo 処理完了マーカーが見つかりません
    exit /b 2
)

rem すべてのチェックをパス
exit /b 0

サブルーチン設計のベストプラクティス

  • すべてのサブルーチンは exit /b N明示的に終了コードを返す
  • 正常終了は exit /b 0、エラーは exit /b 1(または原因別の値)
  • goto :EOF は ERRORLEVEL を変更しない。意図的に値を制御するなら exit /b を使う
  • 複数のチェックを行うサブルーチンでは、各チェック失敗時に異なる終了コードを返すと呼び出し元でのデバッグが容易になる

チェック10:call / start の ERRORLEVEL 伝播仕様を誤解していないか

callstart では ERRORLEVEL の扱いがまったく異なります。この違いを理解していないと、非同期処理のエラー判定で「常に 1」や「常に 0」という問題が発生します。

コマンド ERRORLEVEL の伝播 実行方式 注意点
call script.bat 呼び出し先の終了コードが呼び出し元に伝播する 同期(待機) exit /b の値がそのまま返る
start script.bat 伝播しない(常に 0) 非同期(別窓) エラー判定できない
start /wait script.bat 呼び出し先の終了コードが伝播する 同期(待機) exit の値が返る(exit /b ではない)

call 時に ERRORLEVEL が常に 1 になるケース

問題 ─ 存在しないバッチファイルを call した場合
@echo off

rem 存在しないスクリプトを call
call nonexist.bat
echo ERRORLEVEL = %ERRORLEVEL%

rem 出力: ERRORLEVEL = 1
rem call で見つからないファイルを指定すると ERRORLEVEL = 1

呼び出し先のバッチファイルが存在しない場合、call は ERRORLEVEL を 1 に設定します。パスの typo やファイルの配置ミスが原因で、意図せず ERRORLEVEL が常に 1 になることがあります。

修正方法:呼び出し前にファイル存在確認を行う

修正後 ─ ファイル存在確認後に call
@echo off

set SCRIPT=C:\scripts\process.bat

if not exist "%SCRIPT%" (
    echo スクリプトが見つかりません: %SCRIPT%
    exit /b 1
)

call "%SCRIPT%"
if %ERRORLEVEL% neq 0 (
    echo スクリプトの実行でエラーが発生しました
    exit /b %ERRORLEVEL%
)

チェック11:リダイレクトのエラーが ERRORLEVEL に影響していないか

コマンドのリダイレクト先(出力ファイル)に問題がある場合、コマンド自体は成功してもリダイレクトの失敗で ERRORLEVEL が 1 になることがあります。

問題が起きるコード

問題 ─ リダイレクト先が書き込み不可
@echo off

rem 読み取り専用のファイルにリダイレクト
dir C:\ > C:\readonly_file.txt
echo ERRORLEVEL = %ERRORLEVEL%

rem 存在しないフォルダへリダイレクト
dir C:\ > C:\nonexist_folder\output.txt
echo ERRORLEVEL = %ERRORLEVEL%

リダイレクト先のファイルが読み取り専用だったり、ディレクトリが存在しない場合、ERRORLEVEL が 1 になります。コマンド自体(dir)は正常に実行されているため、原因に気づきにくいのが特徴です。

修正方法:リダイレクト先の存在確認と権限チェック

修正後 ─ リダイレクト先を事前に確認
@echo off

set OUTPUT_DIR=C:\logs
set OUTPUT_FILE=%OUTPUT_DIR%\output.txt

rem 出力先ディレクトリの存在確認
if not exist "%OUTPUT_DIR%" (
    mkdir "%OUTPUT_DIR%"
)

rem 書き込みテスト
echo test > "%OUTPUT_FILE%" 2> nul
if %ERRORLEVEL% neq 0 (
    echo 出力先に書き込めません: %OUTPUT_FILE%
    exit /b 1
)

rem 本処理
dir C:\ > "%OUTPUT_FILE%"

チェック12:FORループ内で ERRORLEVEL が固定されていないか

チェック6の遅延展開問題は、for ループ内でも同様に発生します。for の本体(do (...) 部分)は括弧ブロックなので、ループ開始時の ERRORLEVEL がすべての繰り返しで使われてしまうのです。

問題が起きるコード

問題 ─ FOR 内で %ERRORLEVEL% が固定される
@echo off

rem ループの前に ERRORLEVEL = 1 にしておく
find "NOTEXIST" dummy.txt > nul 2>&1

for %%f in (file1.txt file2.txt file3.txt) do (
    copy %%f backup\ > nul 2>&1
    rem copy が成功しても %ERRORLEVEL% は 1 のまま
    echo %%f: ERRORLEVEL = %ERRORLEVEL%
)

実行結果(全ファイルのコピーが成功した場合)

file1.txt: ERRORLEVEL = 1
file2.txt: ERRORLEVEL = 1
file3.txt: ERRORLEVEL = 1

すべてのコピーが成功しているにもかかわらず、ループ前の ERRORLEVEL = 1 がそのまま表示されています。

修正方法:遅延展開を使う

修正後 ─ FOR 内で !ERRORLEVEL! を使う
@echo off
setlocal enabledelayedexpansion

set ERROR_COUNT=0

for %%f in (file1.txt file2.txt file3.txt) do (
    copy %%f backup\ > nul 2>&1

    rem !ERRORLEVEL! なら各コマンドの実際の結果を取得できる
    if !ERRORLEVEL! neq 0 (
        echo %%f のコピーに失敗しました
        set /a ERROR_COUNT+=1
    ) else (
        echo %%f のコピーに成功しました
    )
)

echo 完了(エラー件数: !ERROR_COUNT!)

実行結果

file1.txt のコピーに成功しました
file2.txt のコピーに成功しました
file3.txt のコピーに成功しました
完了(エラー件数: 0)

ポイント:FOR ループ内で ERRORLEVEL を確認する場合は、必ず setlocal enabledelayedexpansion!ERRORLEVEL! を使いましょう。%ERRORLEVEL% ではループ内の値が更新されません。エラーカウントなどの変数も同様に ! で参照する必要があります。

原因を体系的に切り分ける ── 診断スクリプト

チェックリストを上から順に確認しても原因が特定できない場合は、以下の診断スクリプトを使って ERRORLEVEL の挙動を体系的に確認できます。

diagnose_errorlevel.bat ─ ERRORLEVEL 診断スクリプト
@echo off
setlocal enabledelayedexpansion

echo ============================================
echo  ERRORLEVEL 診断スクリプト
echo ============================================
echo.

rem ── チェック1: ユーザー変数の存在確認 ──
echo [1] ユーザー変数 ERRORLEVEL の確認
set ERRORLEVEL > nul 2>&1
if !ERRORLEVEL! equ 0 (
    echo     警告: ユーザー変数 ERRORLEVEL が存在しています!
    echo     対処: set ERRORLEVEL= で削除してください
) else (
    echo     OK: ユーザー変数は存在しません
)
echo.

rem ── チェック2: 基本的な ERRORLEVEL リセット ──
echo [2] ERRORLEVEL リセットの確認
cmd /c exit /b 0
echo     cmd /c exit /b 0 の後: !ERRORLEVEL!
if !ERRORLEVEL! neq 0 (
    echo     異常: リセットできません。環境に問題があります
) else (
    echo     OK: 正常にリセットできます
)
echo.

rem ── チェック3: 遅延展開の動作確認 ──
echo [3] 遅延展開の動作確認
cmd /c exit /b 0
if 1==1 (
    find "DIAG_TEST_STRING" "%~f0" > nul
    echo     括弧内 %%ERRORLEVEL%%: %ERRORLEVEL%
    echo     括弧内 ^^!ERRORLEVEL^^!: !ERRORLEVEL!
)
echo.

rem ── チェック4: 各コマンドの ERRORLEVEL ──
echo [4] 主要コマンドの ERRORLEVEL 確認
dir C:\ > nul 2>&1
echo     dir C:\          : !ERRORLEVEL!
ver > nul
echo     ver              : !ERRORLEVEL!
find "ZZZZZZZ" "%~f0" > nul 2>&1
echo     find(不一致)   : !ERRORLEVEL!
echo.

echo ============================================
echo  診断完了
echo ============================================
pause

正常時の実行結果

============================================
 ERRORLEVEL 診断スクリプト
============================================
.
[1] ユーザー変数 ERRORLEVEL の確認
    OK: ユーザー変数は存在しません
.
[2] ERRORLEVEL リセットの確認
    cmd /c exit /b 0 の後: 0
    OK: 正常にリセットできます
.
[3] 遅延展開の動作確認
    括弧内 %ERRORLEVEL%: 0
    括弧内 !ERRORLEVEL!: 0
.
[4] 主要コマンドの ERRORLEVEL 確認
    dir C:\          : 0
    ver              : 0
    find(不一致)   : 1
.
============================================
 診断完了
============================================

このスクリプトで以下の点を確認できます。

診断項目 正常 異常時の対処
ユーザー変数の有無 存在しない set ERRORLEVEL= で削除
リセット可否 0 になる 環境の再構築が必要
遅延展開の動作 % と ! で値が異なる setlocal enabledelayedexpansion の確認
コマンドの終了コード 期待通りの値 各コマンドの仕様を確認

実務で使える防御的コーディングパターン

ここまでのチェックリストで原因を特定・修正できたら、今後同じ問題が起きないように防御的なコーディングパターンを身につけましょう。

パターン1:コマンド実行直後に即座に判定する

パターン1 ─ 即座判定テンプレート
rem コマンド実行 → 直後に判定 → 変数に保存
somecommand args
set CMD_RESULT=%ERRORLEVEL%

rem 以降は CMD_RESULT で判定(ERRORLEVEL は上書きされてもOK)
if %CMD_RESULT% neq 0 (
    echo エラー発生: %CMD_RESULT%
)

パターン2:|| 演算子で失敗時の即時ジャンプ

パターン2 ─ || による即時エラーハンドリング
@echo off

copy source.txt dest.txt > nul 2>&1 || goto :ERR_COPY
move temp.dat archive\ > nul 2>&1 || goto :ERR_MOVE
del /q temp\*.tmp > nul 2>&1 || goto :ERR_DEL

echo すべての処理が正常完了
exit /b 0

:ERR_COPY
echo エラー: ファイルのコピーに失敗しました
exit /b 1

:ERR_MOVE
echo エラー: ファイルの移動に失敗しました
exit /b 2

:ERR_DEL
echo エラー: 一時ファイルの削除に失敗しました
exit /b 3

パターン3:遅延展開+エラーカウントの統合テンプレート

パターン3 ─ ループ内エラーカウント
@echo off
setlocal enabledelayedexpansion

set TOTAL=0
set SUCCESS=0
set FAIL=0

for %%f in (C:\data\*.csv) do (
    set /a TOTAL+=1

    copy "%%f" C:\backup\ > nul 2>&1

    if !ERRORLEVEL! equ 0 (
        set /a SUCCESS+=1
    ) else (
        set /a FAIL+=1
        echo 失敗: %%f
    )
)

echo 処理結果: 合計 !TOTAL! / 成功 !SUCCESS! / 失敗 !FAIL!

if !FAIL! gtr 0 (
    exit /b 1
)
exit /b 0

パターン4:robocopy 対応テンプレート

パターン4 ─ robocopy の終了コードを正規化
@echo off

robocopy "%SOURCE%" "%DEST%" /MIR /R:3 /W:5 /NP
set RC=%ERRORLEVEL%

rem robocopy の終了コードを通常の 0/1 に正規化
if %RC% geq 8 (
    echo [ERROR] robocopy failed with exit code %RC%
    exit /b 1
)

rem 0〜7 は成功扱い
echo [OK] robocopy completed with exit code %RC%
exit /b 0

パターン5:エラーログ付きの堅牢なテンプレート

パターン5 ─ エラーログ付きバッチテンプレート
@echo off
setlocal enabledelayedexpansion

rem ── 初期設定 ──
set LOG_FILE=%~dp0logs\%~n0_%date:~0,4%%date:~5,2%%date:~8,2%.log
set EXIT_CODE=0

rem ── ログディレクトリ作成 ──
if not exist "%~dp0logs" mkdir "%~dp0logs"

rem ── 処理開始 ──
call :LOG "処理開始"

rem ── メイン処理 ──
call :EXEC "copy source.txt dest.txt"
if !ERRORLEVEL! neq 0 set EXIT_CODE=1

call :LOG "処理完了(終了コード: !EXIT_CODE!)"
exit /b !EXIT_CODE!

rem ── サブルーチン: コマンド実行+ログ ──
:EXEC
call :LOG "実行: %~1"
%~1 > nul 2>>&1
set RC=!ERRORLEVEL!
if !RC! neq 0 (
    call :LOG "[ERROR] 終了コード: !RC!"
) else (
    call :LOG "[OK]"
)
exit /b !RC!

rem ── サブルーチン: ログ出力 ──
:LOG
echo %date% %time% %~1
echo %date% %time% %~1 >> "%LOG_FILE%"
exit /b 0

どうしても切り分けられないときの最終手段

ここまでの12のチェックと診断スクリプトで原因が特定できない場合は、以下の最終確認を行います。

手順1:ERRORLEVEL を強制的に 0 にリセットする

最終確認 ─ 環境自体の問題を切り分け
@echo off

rem ステップ1: ユーザー変数を削除
set ERRORLEVEL=

rem ステップ2: ERRORLEVEL を 0 に強制リセット
cmd /c exit /b 0

rem ステップ3: 確認
echo リセット後: %ERRORLEVEL%

rem ここで 0 にならない場合
rem → cmd.exe 自体の問題、またはシステム環境変数の問題

手順2:新しいcmd.exeで確認

親プロセスから ERRORLEVEL が引き継がれている可能性があります。新しい cmd ウィンドウを開いて同じスクリプトを実行し、結果が変わるか確認してください。

手順3:バッチファイルの文字コード・改行コードを確認

UTF-8 BOM付きのファイルや、LF(Unix改行)のファイルでは、バッチファイルのパースが正常に行われないことがあります。

確認項目 推奨 問題が起きる設定
文字コード Shift_JIS / ANSI UTF-8(特にBOM付き)
改行コード CR+LF(Windows標準) LF のみ(Unix/Mac)
BOM なし あり(UTF-8 BOM)

注意:近年のテキストエディタ(VS Code等)はデフォルトで UTF-8 を使用します。バッチファイルを保存する際は、エンコーディングを Shift_JIS(または ANSI)、改行を CRLF に設定してください。

チェックリスト早見表

最後に、本記事で紹介した12のチェック項目を一覧表にまとめます。ERRORLEVEL が常に 1 になる問題に遭遇したら、この表の上から順に確認してください。

No. チェック項目 修正の方向性
1 コマンド自体が 1 を返す仕様ではないか コマンドの終了コード仕様を確認し、判定ロジックを合わせる
2 ERRORLEVEL を更新しないコマンドが挟まっていないか cmd /c exit /b 0 でリセット、または判定直前を確認
3 判定コマンドとの間に他のコマンドがないか 対象コマンドの直後で判定する。|| の活用
4 パイプの後段コマンドが原因ではないか パイプを分解して各段で判定する
5 if ERRORLEVEL N の「以上比較」を誤解していないか %ERRORLEVEL% equ N の等号比較を使う
6 括弧ブロック内で %ERRORLEVEL% が固定されていないか setlocal enabledelayedexpansion + !ERRORLEVEL!
7 robocopy 等の特殊な終了コードではないか コマンド固有の判定ロジックを実装する
8 set ERRORLEVEL= でユーザー変数を作っていないか 値なし set ERRORLEVEL= で削除する
9 サブルーチンの戻り値が設計されているか exit /b 0 で明示的に終了コードを返す
10 call / start の ERRORLEVEL 伝播を理解しているか 呼び出し先の存在確認、start /wait の検討
11 リダイレクト先に問題がないか 出力先ディレクトリの存在確認と権限チェック
12 FOR ループ内で ERRORLEVEL が固定されていないか 遅延展開を有効にして !ERRORLEVEL! を使う

よくある質問(FAQ)

Q. IF ERRORLEVEL 1が正しく動かないのはなぜですか?
A. IF ERRORLEVEL 1は「ERRORLEVELが1以上なら真」という意味です(「1のとき」ではない)。そのため、ERRORLEVELが2や3でも真になります。特定の値だけ判定したい場合はIF %ERRORLEVEL% EQU 1のように使います。
Q. xcopyやrobocopyでERRORLEVELが1になるのはエラーですか?
A. いいえ。xcopyはファイルコピー成功時でもERRORLEVEL=1を返す場合があります。robocopyはERRORLEVEL 0(変更なし)・1(コピー成功)・2(追加ファイルあり)など独自の終了コードを持ちます。これらはエラーではなく正常な動作です。
Q. バッチ内でIF ERRORLEVEL判定をCALL後に行うとき注意点はありますか?
A. 遅延展開が有効な場合は%ERRORLEVEL%ではなく!ERRORLEVEL!を使う必要があります。また、CALLした後の次のコマンド実行でERRORLEVELが上書きされる場合があるため、CALL直後にSET EL=%ERRORLEVEL%で値を退避させておくのが安全です。

まとめ

ERRORLEVEL が常に 1 になる問題は、一見すると「何かが壊れている」ように感じますが、ほとんどの場合はERRORLEVEL の仕様を正しく理解していないことが原因です。

本記事のチェックリストを振り返ると、原因は大きく以下の4つのカテゴリに分類されます。

カテゴリ 該当チェック 対策のキーワード
コマンド仕様の誤解 1, 5, 7 各コマンドの終了コード仕様を確認する
更新タイミングの問題 2, 3, 4, 9, 10, 11 判定は対象コマンドの直後で行う
展開タイミングの問題 6, 12 遅延展開(!ERRORLEVEL!)を使う
変数の上書き 8 ERRORLEVEL という名前の変数を作らない

ERRORLEVEL の問題は、原因さえ特定できれば修正は簡単です。本記事のチェックリストを上から順に確認していくことで、確実に原因にたどり着けるはずです。

また、今後の開発では以下の3つの原則を意識すると、ERRORLEVEL に関するトラブルを未然に防ぐことができます。

ERRORLEVEL トラブル防止の3原則

  • 原則1:判定は直後に ── ERRORLEVEL の確認は、対象コマンドの直後で行う。間にコマンドを挟まない
  • 原則2:括弧内は遅延展開 ── if (...)for ... do (...) の中では必ず !ERRORLEVEL! を使う
  • 原則3:仕様を確認 ── 初めて使うコマンドは、終了コードの意味をドキュメントで確認してから判定ロジックを書く

ERRORLEVEL関連記事