13

Consider the following bat, test.bat (PC01 is off):

mkdir \\PC01\\c$\Test || goto :eof

If I run that bat from a command shell:

> test.bat || echo 99
> if ERRORLEVEL 1 echo 55

The output is just 55. No 99. There is an errorlevel, but the || operator did not see it.

If I run that bat using cmd /c -

> cmd /c test.bat || echo 99
> if ERRORLEVEL 1 echo 55

The output is blank. Errorlevel is 0.

If I remove the || goto :eof, everything works as one would predict — i.e the output would be

99 55

Does anyone know why this half-baked semi-existent ERRORLEVEL behaviour is occurring?

jpaugh
  • 6,634
  • 4
  • 38
  • 90
Patrick
  • 1,089
  • 14
  • 17

3 Answers3

30

In most circumstances, || is the most reliable way to detect an error. But you have stumbled on one of the rare cases where ERRORLEVEL works but || does not.

The problem stems from the fact that your error is raised within a batch script, and || responds to the return code of the most recently executed command. You are thinking of test.bat as a single "command", but actually it is a sequence of commands. The last command executed within the script is GOTO :EOF, and that executed successfully. So your test.bat||echo 99 is responding to the success of the GOTO :EOF.

When you remove the ||GOTO :EOF from within the script, then your test.bat||echo99 sees the result of the failed mkdir. But if you were to add a REM command to the end of test.bat, then test.bat||echo 99 would respond to the success of the REM, and the error would be masked again.

The ERRORLEVEL is still non-zero after test.bat||echo 99 because commands like GOTO and REM do not clear any prior non-zero ERRORLEVEL upon success. This is one of many pieces of evidence that ERRORLEVEL and the return code are not quite the same thing. It definitely gets confusing.

You can treat test.bat as a unit command and get the behavior you want by using CALL.

C:\test>call test.bat && echo OK || echo FAIL
FAIL

C:\test>if ERRORLEVEL 1 (echo FAIL2) else echo OK2
FAIL2

This works because the CALL command temporarily transfers control to the called script. When the script terminates, control is returned to the CALL command, and it returns the current ERRORLEVEL. So ||echo 99 is responding to the error returned by the CALL command itself, not the last command within the script.

Now for the CMD /C issue.

The return code returned by CMD /C is the return code of the last command executed.

This works:

C:\test>cmd /c call test.bat && echo OK || echo FAIL
FAIL

C:\test>if ERRORLEVEL 1 (echo FAIL2) else echo OK2
FAIL2

because CMD /C returns the ERRORLEVEL returned by the CALL statement

But this fails entirely:

C:\test>cmd /c test.bat && echo OK || echo FAIL
OK

C:\test>if ERRORLEVEL 1 (echo FAIL2) else echo OK2
OK2

Without the CALL, the CMD /C returns the return code of the last executed command, which is the GOTO :EOF. The CMD /C also sets the ERRORLEVEL to the same return code, so now there is no evidence that there ever was an error within the script.

And down the rabbit hole we go

R.L.H., in his answer and in his comments to my answer, is concerned that || sometimes clears the ERRORLEVEL. He provides evidence that appears to back up his conclusion. But the situation isn't that simple, and it turns out that || is the most reliable (but still not perfect) way to detect errors.

As I stated earlier, the return code that all external commands return upon exit is not the same thing as the cmd.exe ERRORLEVEL.

The ERRORLEVEL is a state maintained within the cmd.exe session itself, wholly distinct from return codes.

This is even documented in the definition of the exitCode within the EXIT help
(help exit or exit /?)

EXIT [/B] [exitCode]

  /B          specifies to exit the current batch script instead of
              CMD.EXE.  If executed from outside a batch script, it
              will quit CMD.EXE

  exitCode    specifies a numeric number.  if /B is specified, sets
              ERRORLEVEL that number.  If quitting CMD.EXE, sets the process
              exit code with that number.

When an external command is run by CMD.EXE, it detects the executeable's return code and sets the ERRORLEVEL to match. Note that it is only a convention that 0 means success, and non-zero means error. Some external commands may not follow that convention. For example, the HELP command (help.exe) does not follow the convention - it returns 0 if you specify an invalid command as in help bogus, but returns 1 if you ask for help on a valid command, as in help rem.

The || operator never clears the ERRORLEVEL when an external command is executed. The process exit code is detected and fires || if it is non-zero, and the ERRORLEVEL will still match the exit code. That being said, the commands that appear after && and/or || may modify the ERRORLEVEL, so one has to be careful.

But there are many other situations besides external commands where we developers care about success/failure and return codes/ERRORLEVELs.

  • execution of internal commands
  • redirection operators <, >, and >>
  • execution of batch scripts
  • failed execution of invalid commands

Unfortunately, CMD.EXE is not at all consistent in how it handles error conditions for these situations. CMD.EXE has multiple internal points where it must detect errors, presumably through some form of internal return code that is not necessarily the ERRORLEVEL, and at each of these points CMD.EXE is in a position to set the ERRORLEVEL depending on what it finds.

For my test cases below, note that (call ), with a space, is arcane syntax that clears the ERRORLEVEL to 0 before each test. Later on, I will also use (call), without a space, to set the ERRORLEVEL to 1

Also note that delayed expansion has been enabled within my command session by using
cmd /v: on prior to running my tests

The vast majority of internal commands set the ERRORLEVEL to a non-zero value upon failure, and the error condition also fires ||. The || never clears or modifies the ERRORLEVEL in these cases. Here are a couple examples:

C:\test>(call ) & set /a 1/0
Divide by zero error.

C:\test>echo !errorlevel!
1073750993

C:\test>(call ) & type notExists
The system cannot find the file specified.

C:\test>echo !errorlevel!
1

C:\test>(call ) & set /a 1/0 && echo OK || echo ERROR !errorlevel!
Divide by zero error.
ERROR 1073750993

C:\test>(call ) & type notExists.txt && echo OK || echo ERROR !errorlevel!
The system cannot find the file specified.
ERROR 1

Then there is at least one command, RD, (possibly more), as well as the redirection operators that fire || upon error, but do not set ERRORLEVEL unless || is used.

C:\test>(call ) & rd notExists
The system cannot find the file specified.

C:\test>echo !errorlevel!
0

C:\test>(call ) & echo x >\badPath\out.txt
The system cannot find the path specified.

C:\test>echo !errorlevel!
0

C:\test>(call ) & rd notExists && echo OK || echo ERROR !errorlevel!
The system cannot find the file specified.
ERROR 2

C:\test>(call ) & echo x >\badPath\out.txt && echo OK || echo ERROR !errorlevel!
The system cannot find the path specified.
ERROR 1

See "rd" exits with errorlevel set to 0 on error when deletion fails, etc and File redirection in Windows and %errorlevel% for more information.

I know of one internal command (there may be others) plus basic failed I/O operations that can issue error messages to stderr, yet they don't fire || nor do they set a non-zero ERRORLEVEL.

The DEL command can print an error if the file is read only, or does not exist, but it does not fire || or set ERRORLEVEL to non-zero

C:\test>(call ) & del readOnlyFile
C:\test\readOnlyFile
Access is denied.

C:\test>echo !errorlevel!
0

C:\test>(call ) & del readOnlyFile & echo OK || echo ERROR !errorlevel!
C:\test\readOnlyFile
Access is denied.
OK

See https://stackoverflow.com/a/32068760/1012053 for a bit more information related to DEL errors.

In much the same way, when stdout has been successfully redirected to a file on a USB device, but then the device is removed before a command such as ECHO tries to write to the device, then the ECHO will fail with an error message to stderr, yet || does not fire, and ERRORLEVEL is not set to non-zero. See http://www.dostips.com/forum/viewtopic.php?f=3&t=6881 for more info. 

Then we have the case where a batch script is executed - the actual subject of the OP's question. Without CALL, The || operator responds to the last command executed within the script. With CALL, the || operator responds to the value returned by the CALL command, which is the final ERRORLEVEL that exists upon batch termination.

Finally, we have the case that R.L.H. reports, where an invalid command is reported as ERRORLEVEL 9009 normally, but as ERRORLEVEL 1 if || is used.

C:\test>(call ) & InvalidCommand
'InvalidCommand' is not recognized as an internal or external command,
operable program or batch file.

C:\test>echo !errorlevel!
9009

C:\test>(call ) & InvalidCommand && echo OK || echo ERROR !errorlevel!
'InvalidCommand' is not recognized as an internal or external command,
operable program or batch file.
ERROR 1

I can't prove this, but I suspect that the detection of command failure and setting of ERRORLEVEL to 9009 occurs very late in the command execution process. I'm guessing that || intercepts the error detection before the 9009 is set, at which point it sets it to 1 instead. So I don't think || is clearing the 9009 error, but rather it is an alternate pathway by which the error is handled and set.

An alternate mechanism for this behavior is that the invalid command could always set the ERRORLEVEL to 9009, yet have a different return code of 1. The || could subsequently detect the 1 return code and set the ERRORLEVEL to match, thus overwriting the 9009.

Regardless, I am not aware of any other situation where a non-zero ERRORLEVEL result is different depending on whether || was used or not.

So that takes care of what happens when a command fails. But what about when an internal command succeeds? Unfortunately, CMD.EXE is even less consistent than it was with errors. It varies by command, and may also depend on whether it is executed from the command prompt, from a batch script with a .bat extension, or from a batch script with a .cmd extension.

I am basing all of the discussion below on Windows 10 behavior. I doubt there are differences with earlier Windows versions that use cmd.exe, but it is possible.

The following commands always clear the ERRORLEVEL to 0 upon success, regardless of context:

  • CALL : Clears ERRORLEVEL if the CALLed command does not otherwise set it.
    Example: call echo OK
  • CD
  • CHDIR
  • COLOR
  • COPY
  • DATE
  • DEL : Always clears ERRORLEVEL, even if the DEL fails
  • DIR
  • ERASE : Always clears ERRORLEVEL, even if ERASE fails
  • MD
  • MKDIR
  • MKLINK
  • MOVE
  • PUSHD
  • REN
  • RENAME
  • SETLOCAL
  • TIME
  • TYPE
  • VER
  • VERIFY
  • VOL

The next set of commands never clear the ERRORLEVEL to 0 upon success, regardless of context, but instead preserve any existing non-zero value ERRORLEVEL:

  • BREAK
  • CLS
  • ECHO
  • ENDLOCAL
  • EXIT : Obviously EXIT /B 0 clears the ERRORLEVEL, but EXIT /B without value preserves the prior ERRORLEVEL.
  • FOR
  • GOTO
  • IF
  • KEYS
  • PAUSE
  • POPD
  • RD
  • REM
  • RMDIR
  • SHIFT
  • START
  • TITLE

And then there are these commands that do not clear ERRORLEVEL upon success if issued from the command line or within a script with a .bat extension, but do clear the ERRORLEVEL to 0 if issued from a script with a .cmd extension. See https://stackoverflow.com/a/148991/1012053 and https://groups.google.com/forum/#!msg/microsoft.public.win2000.cmdprompt.admin/XHeUq8oe2wk/LIEViGNmkK0J for more info.

  • ASSOC
  • DPATH
  • FTYPE
  • PATH
  • PROMPT
  • SET

Regardless of any ERRORLEVEL value, the && operator detects if the prior command was successful, and only executes the subsequent command(s) if it was. The && operator ignores the value of ERRORLEVEL, and never modifies it.

Here are two examples that show that && always fires if the prior command was successful, even if the ERRORLEVEL is non-zero. The CD command is an example where the command clears any prior ERRORLEVEL, and the ECHO command is an example where the command does not clear the prior ERRORLEVEL. Note, I am using (call) to force ERRORLEVEL to 1 before issuing the command that succeeds.

C:\TEST>(call)

C:\TEST>echo !errorlevel!
1

C:\test>(call) & cd \test

C:\test>echo !errorlevel!
0

C:\test>(call) & cd \test && echo OK !errorlevel! || echo ERROR !errorlevel!
OK 0

C:\test>(call) & echo Successful command
Successful command

C:\test>echo !errorlevel!
1

C:\test>(call) & echo Successful command && echo OK !errorlevel! || echo ERROR !errorlevel!
Successful command
OK 1

In all of my code examples for error detection, I was relying on the fact that ECHO never clears a previously existing non-zero ERRORLEVEL. But the script below is an example of what can happen when other commands are used after && or ||.

@echo off
setlocal enableDelayedExpansion
(call)
echo ERRORLEVEL = !errorlevel!
(call) && echo OK !errorlevel! || echo ERROR !errorlevel!
(call) && (echo OK !errorlevel! & set "err=0") || (echo ERROR !errorlevel! & set "err=1" & echo ERROR !errorlevel!)
echo ERRORLEVEL = !errorlevel!
echo ERR = !ERR!

Here is the output when the script has a .bat extension:

C:\test>test.bat
ERRORLEVEL = 1
ERROR 1
ERROR 1
ERROR 1
ERRORLEVEL = 1
ERR = 1

And here is the output when the script has a .cmd extension:

C:\test>test.cmd
ERRORLEVEL = 1
ERROR 1
ERROR 1
ERROR 0
ERRORLEVEL = 0
ERR = 1

Remember that every executed command has the potential to alter the ERRORLEVEL. So even though && and || are the most reliable ways to detect command success or failure, one must be careful about what commands are used after those operators if you care about the ERRORLEVEL value.

And now it is time to climb out of this stinking rabbit hole and get some fresh air!

So what have we learned?

There is no single perfect method to detect whether any arbitrary command was successful or failed. However, && and || are the most reliable methods to detect success and failure.

In general, neither && nor || modify the ERRORLEVEL directly. But there are a few rare exceptions.

  • || properly sets the ERRORLEVEL that would otherwise be missed when RD or redirection fails
  • || sets a different ERRORLEVEL upon failed execution of an invalid command then would occur if || were not used (1 vs. 9009).

Finally, || does not detect a non-zero ERRORLEVEL returned by a batch script as an error unless the CALL command was used.

If you rely strictly on if errorlevel 1 ... or if %errorlevel% neq 0 ... to detect errors, then you run the risk of missing errors that RD and redirection (and others?) might throw, and you also run the risk of mistakenly thinking certain internal commands failed when in reality it could be a holdover from a prior command that failed.

dbenham
  • 127,446
  • 28
  • 251
  • 390
  • Its not only calling batch files. I get the same test results of an errorlevel 1 inside a single batch file. ` || echo %errorlevel%` then next line `echo %errorlevel%`, and the first one is different than the second. – RLH Jan 22 '16 at 01:57
  • Hey indeed 'call' does make the || work! I'm stuck with cmd /c as I am launching from c#.. wait .. looks like i can use 'call test.bat' from c#... – Patrick Jan 22 '16 at 01:59
  • @RLH - That is because `%errorlevel%` is expanded when the command is parsed, and the entire line is parsed in one pass. You must use delayed expansion (must be enabled first) to do what you tried ` || echo !errorlevel!` works perfectly – dbenham Jan 22 '16 at 02:00
  • @RLH - Within a batch script, you enable delayed expansion via `setlocal enableDelayedExpansion` – dbenham Jan 22 '16 at 02:01
  • Yes. Call is the solution - the thing is I am happy to code a bat to do the whole "exit /b %errorlevel%, but the scripts may come from other admins. SOrry maybe that should be part of the question – Patrick Jan 22 '16 at 02:04
  • "pieces of evidence" and not a single pice of documentation lol :/ – Patrick Jan 22 '16 at 02:06
  • @dbenham, i may not have been very clear in my info. What i see is that no matter what command is failure piped via || i then get an errorlevel of 1 in my next test. For example a program not found that gives 9009 in my next test shows 1 after echo is piped, even though echo doesn't reset it if I put it on another line. (edit, I did test the delayed expansion to make sure as well) – RLH Jan 22 '16 at 02:09
  • 1
    @Patrick - I have no idea. I think your question is actually a very good one, so I just up voted it. – dbenham Jan 22 '16 at 02:20
  • @dbenham thanks for that. I've bookmarked your SE, right next to ss64 :) – Patrick Jan 22 '16 at 02:32
  • I suppose you are right with your guess at the `InvaludCommand` logic. I tried `cmd /c InvalidCommand` and it sets the errorlevel always to 1, but `cmd /c exit /b 9999` works – jeb Jan 25 '16 at 10:22
  • 1
    DEL sets the errorlevel only upon argument error, invalid path *format*, and non-existent drives. [My SO answer of this point](http://stackoverflow.com/a/32068760/4606310). – Explorer09 Sep 19 '16 at 16:01
  • EDIT - Improved/corrected the explanation for why `call test.bat || echo fail` "works", but `test.bat || echo fail` does not. – dbenham Dec 24 '16 at 12:24
  • Also the `for /F` command sets the exit code to a non-zero value in case the parsed file/string/command output is empty, but it does not set the `ErrorLevel` in such a case; this is another evidence that exit code and `ErrorLevel` are different things... – aschipfl Jan 04 '17 at 22:52
  • ...placing `||` behind the `(for /F)` command sets the `ErrorLevel`, just like for the `rd` command... – aschipfl Jan 04 '17 at 23:02
  • Do you have a link / screen showing that `TIME` resets the errorlevel. when i call it in cmd, windows 10 and server 2012 it appears to leave the errorlevel – Michael Blake May 24 '19 at 11:29
  • @MichaelBlake - The only way I know for TIME to fail is if you issue the command without an argument or with an invalid time, and stdin is redirected to nul, or redirected/piped stdin is exhausted before a valid time is received, or you press at the prompt. Of course in these error cases, TIME sets ERRORLEVEL to 1. But in all successful cases, TIME sets ERRORLEVEL to 0 for me: `TIME /T`, or `TIME validTime`, or `TIME` followed by valid time at prompt. What command sequence are you using that results in successful TIME failing to clear the ERRORLEVEL? – dbenham May 24 '19 at 13:15
  • @MichaelBlake - Another error case is if you attempt to set the time without admin privilege. Again this error condition sets ERRORLEVEL to 1. So in summary, TIME always sets the ERRORLEVEL to 1 upon error or 0 upon success. – dbenham May 24 '19 at 13:17
  • In a cmd file, one Windows 2012 in cloud services https://learn.microsoft.com/en-us/azure/cloud-services/cloud-services-startup-tasks-common#detect-that-your-task-has-already-run – Michael Blake May 24 '19 at 15:49
  • 1
    @MichaelBlake - Both DATE and TIME clear the ERRORLEVEL to 0 in that code, but you don't see that in the output because the DATE, TIME, and ECHO commands are all in the same code block. Percent expansion occurs at parse time, and the entire block is parsed at once. So the %ERRORLEVEL% you see is the value that existed before the block was entered (before DATE or TIME have executed). If you ECHO %ERRORLEVEL% after the close of the block, then you will see the updated value of 0. Or if you enabled delayed expansion and used ECHO !ERRORLEVEL!, then you will see 0 within the code block. – dbenham May 24 '19 at 20:10
3

To build a solution for batch files to set the return code while using goto :eof you can change your script a bit.

mkdir \\\failure || goto :EXIT
echo Only on Success
exit /b

:exit
(call)

Now you can use

test.bat || echo Failed

The only drawback here is the loss of the errorlevel.
In this case the return code is set to false but the errorlevel is always set to 1, by the invalid (call)
Currently I can't found any possible way to set both, the errorlevel and the return code to user defined values

jeb
  • 78,592
  • 17
  • 171
  • 225
  • Thanks jeb. Yeah look the fix for me was to always call the bat files with cmd /c call xx.bat. The 'call' being the magic glue. My situation is that the invocation of the bat is under my control, the contents of the bat file are not. – Patrick Feb 03 '16 at 02:23
  • 1
    +1 , Thanks jeb. Your answer forced me to re-examine how `||` responds to a batch script without CALL. It allowed me to rediscover that `||` without CALL responds to the last executed command within the script. It is something that I had known before, but forgotten. I've edited my answer to properly explain the difference between using CALL and not using CALL. – dbenham Dec 24 '16 at 12:31
1

The true Errorlevel doesn't survive the double pipe (on failure) operator.

Use logic instead. For example, below is one way you could do it:

mkdir \\PC01\c$\Test
if "%errorlevel%" == "0" (
echo success
) else (
echo failure
)

edit:

If the command that precedes this is not a single command then more has to be done to track errorlevel. Example if you call a script, then in that script you have to manage the passing of errorlevel back to this code.

edit 2:

Here are test scenarios where the ERRORLEVEL should be '9009' and their output.

1) No failure piping, uses if logic.

setlocal enableDelayedExpansion
mybad.exe 
if "!errorlevel!" == "0" (
echo success !errorlevel!
) else (
echo Errorlevel is now !errorlevel!
echo Errorlevel is now !errorlevel!
)

'mybad.exe' is not recognized as an internal or external command, operable program or batch file.

Errorlevel is now 9009

Errorlevel is now 9009

2) Failure piping, echo

setlocal enableDelayedExpansion
mybad.exe || echo Errorlevel is now !errorlevel!
echo Errorlevel is now !errorlevel!

'mybad.exe' is not recognized as an internal or external command, operable program or batch file.

Errorlevel is now 1

Errorlevel is now 1

3) Failure piping, goto

setlocal enableDelayedExpansion
mybad.exe || goto :fail
exit
:fail
echo Errorlevel is now !errorlevel!

'mybad.exe' is not recognized as an internal or external command, operable program or batch file.

Errorlevel is now 1

So if all you care about is "Something" failed, then OK. Code 1 is as good as anything else. But if you need to know "what" failed, then you can't just failure pipe and let it wipe out the actual result.

RLH
  • 1,545
  • 11
  • 12
  • The || after the mkdir within the test.bat works perfectly fine – Patrick Jan 22 '16 at 00:30
  • My tests on various commands didn't give consistent good results so from my perspective it's not reliable. To test, change the followup commands to `echo %errorlevel%` instead of what you have. That will show you for each instance what happens. What I posted always works. However if you are calling a batch file, remember to pass the errorlevel out the exit statement like this `exit %errorlevel%`. – RLH Jan 22 '16 at 00:37
  • Looks like it is the || that 'eats it up', not the goto. However, the errorlevel survives within the bat, it just doesn't get returned.. it's like there is two errorlevels in memory – Patrick Jan 22 '16 at 01:15
  • 1
    Everything about this answer is false. `||` does not clear the ERRORLEVEL, nor does `GOTO :EOF`. The modifed code does not solve the problem either. – dbenham Jan 22 '16 at 01:33
  • @dbenham, when i tested, not all of my tests allowed the errorlevel to move through that process of the `||`. For example, fail the call with a program not found as just one example. My example will work for single commands and as I noted in the comment above you have to pass the errorlevel out of a batch correctly if you are using a script instead of a single command. – RLH Jan 22 '16 at 01:36
  • I used his example of ` || echo something` then on the next line test errorlevel and that test for errorlevel is always 1. – RLH Jan 22 '16 at 01:50
  • 1
    @dbenham But if the test.bat contains just the mkdir command, with no || treatment, then the 99, 55 test works fine - so it certainly seem like || is 'eating' it? I dunno. None of this makes sense, clearly ^_^ – Patrick Jan 22 '16 at 02:31
  • @dbenham, Your statement above "_Everything about this answer is false. || does not clear the ERRORLEVEL, nor does GOTO :EOF. The modifed code does not solve the problem either._" is completely incorrect. Look at my examples. The errorlevel is not preserved through the failure piped command. It is cleared to '1'. – RLH Jan 22 '16 at 02:57
  • Please note, `||` has nothing to do with pipes - it is an operator for conditional command concatenation that only fires when the prior command failed. I think you know that, but using the term pipe will likely confuse people. And my statement is not wrong - `||` is not clearing the ERRORLEVEL. But it is complicated. I'm working on a significant expansion to my answer to try to explain things better. But it is going to take some time. – dbenham Jan 22 '16 at 03:37
  • Ok, fine. trapping or whatever we will call it. But the errorlevel is reset to 1. it does not survive in it's true value. my examples are very simple. – RLH Jan 22 '16 at 03:40
  • 2
    @Patrick and R L H - I've finished updating my answer to explain why I consider this answer to be wrong/misleading. I told you it was complicated ! I understand why you came to your conclusions, and your reasoning was sound in the absence of other information. But, unfortunately, batch is not that simple. Please don't take my down vote personally. It is strictly a reflection of the content of the answer. It is not a reflection of your effort. You actually did some good tests that lead you to the wrong conclusion. – dbenham Jan 22 '16 at 14:38