4. Error Handling & Exit Codes¶
What & why¶
A script that keeps going after a step fails will happily deploy broken code or delete the wrong thing. Batch's error model is errorlevel — a single integer left behind by the last command — plus the && / || operators. It has enough quirks to deserve its own lesson.
errorlevel vs %errorlevel%¶
These are not the same thing:
| Form | What it is |
|---|---|
if errorlevel N |
a syntax: true when the exit code is ≥ N |
%errorlevel% |
an environment variable holding the last exit code |
somecmd
if errorlevel 1 echo failed (code is 1 or higher)
if %errorlevel% equ 3 echo failed with exactly 3
if %errorlevel% neq 0 echo failed somehow
if errorlevel 0 is always true¶
Every exit code is ≥ 0. To test "succeeded," use one of:
Test high-to-low when branching¶
The %errorlevel% staleness trap¶
%errorlevel% is expanded at parse time (Intermediate Lesson 1). Inside a block it's frozen:
(
failing_command
echo %errorlevel% &rem prints the value from BEFORE the block
if %errorlevel% neq 0 echo caught &rem also stale!
)
Fixes:
rem A) delayed expansion:
setlocal EnableDelayedExpansion
(
failing_command
if !errorlevel! neq 0 echo caught
)
rem B) the bareword syntax isn't stale — it re-reads each time:
(
failing_command
if errorlevel 1 echo caught
)
rem C) check right after, outside the block
if errorlevel N (the keyword form) is not stale — prefer it inside blocks.
Explicitly setting an exit code¶
exit /b 0 &rem success, return from script/subroutine
exit /b 1 &rem generic failure
exit /b %errorlevel% &rem propagate whatever just happened
exit /b returns from the current script or called subroutine. Plain exit (no /b) terminates the entire cmd.exe — closes the window if it was launched for the script. In a subroutine, exit /b N also sets errorlevel to N for the caller.
Resetting errorlevel¶
cmd /c exit 0 &rem forces errorlevel to 0
ver >nul &rem also a reliable "success" that clears it
(call ) &rem sets errorlevel to 0 (obscure but common)
(call) &rem sets errorlevel to 1
set "errorlevel=0" does not work the way you want — it creates a normal variable that shadows the real one until you set "errorlevel=" again. Don't.
&& and ||¶
build.exe && echo BUILD OK
build.exe || (echo BUILD FAILED & exit /b 1)
step1 && step2 && step3 || echo "a step failed"
A && B— runBonly ifAexited0.A || B— runBonly ifAexited non-zero.- They chain left to right;
&&and||bind tighter than&.
Not every command sets errorlevel
Many internal commands (echo, set in .bat, rem, cd to a valid dir, for, if) leave errorlevel unchanged on success — and some don't set it on failure either. &&/|| after them can act on a stale code. Reliable setters: external .exes, findstr/find, xcopy/robocopy, ping, reg, sc, net. When in doubt, test explicitly.
Common per-command notes¶
| Command | Failure signal |
|---|---|
robocopy |
code ≥ 8 is an error (0–7 are success variants) — see Beginner Lesson 7 |
find / findstr |
1 = no match found, 2 = bad syntax/file |
ping |
1 on no reply (but also check output — ping of an unresolvable name may still be 0 on old builds) |
reg query |
1 if the key/value doesn't exist |
sc query |
1060 if the service doesn't exist |
del of a missing file |
0 (!) — check if exist first |
md of an existing dir |
1 |
A robust step pattern¶
@echo off
setlocal EnableExtensions EnableDelayedExpansion
call :step "Restore packages" nuget restore MyApp.sln
call :step "Build" msbuild MyApp.sln /p:Configuration=Release
call :step "Run tests" dotnet test
echo All steps passed.
exit /b 0
:step
set "label=%~1"
shift
set "cmd=%1"
:collect
shift
if not "%~1"=="" ( set "cmd=!cmd! %1" & goto collect )
echo === !label! ===
!cmd!
if errorlevel 1 (
echo.
echo FAILED at step: !label! ^(exit code !errorlevel!^) 1>&2
exit /b 1
)
goto :eof
(Argument re-assembly like this is fragile with quotes — Advanced Lesson 2 does it properly. For real pipelines, many people just write one || exit /b 1 per line.)
Simple and reliable¶
For most scripts this is enough:
Common mistakes¶
if errorlevel 0— always true.%errorlevel%inside( )— stale; useif errorlevel Nor!errorlevel!.set errorlevel=0— creates a shadow variable and hides the real exit code from then on.- Trusting
&&/||afterecho/set/cd— they may not update errorlevel. del missing.txtthen|| ...—delreturns0for a missing file.robocopywithif errorlevel 1— flags a successful copy as failure.- Not propagating — a subroutine fails, but the script
exit /b 0s anyway. End withexit /b %errorlevel%or check. exitvsexit /b—exitcloses the whole window, taking your logs with it.
Exercises¶
- Run
findstr foo nul(fails), then show three ways to detect the failure:if errorlevel 1,if %errorlevel% neq 0, and||. - Write a block that runs a failing command inside
( )and correctly reports the code both withif errorleveland with!errorlevel!(delayed). Show the stale%errorlevel%version failing. - Chain three commands with
&&so that a failure in step 2 skips step 3 and the script exits non-zero. - Call
robocopyto mirror a folder and classify the result: nothing-to-do / copied / real-error, using the numeric ranges. - Write
:require_cmd NAMEthat checkswhere NAME >nul 2>nulandexit /b 1with a message if the tool isn't installed. Use it to requiregitandcurl. - Demonstrate the
set "errorlevel=5"shadow bug and then un-break it.
Recap¶
if errorlevel N= "code ≥ N" (soif errorlevel 0is useless);%errorlevel%= the actual number.- Inside
( )blocks%errorlevel%is stale — useif errorlevel Nor!errorlevel!. exit /b Nreturns with code N; plainexitkills the shell.A && Bruns B on success;A || Bruns B on failure. Don't trust them afterecho/set/cd.- Know the per-command conventions:
robocopy≥ 8,find/findstr= 1 for no-match,delof a missing file = 0. - Simplest reliable pattern: one
|| exit /b 1per step.