Exiting Batch File Contexts

May 22, 2008

While working on a Windows batch script earlier today, I ran across an interesting side effect of the call and exit commands. Let's take this simple example, which we'll name script_a.bat:

@echo off
SETLOCAL

call :function
cd %SOME_PATH%

goto :functionEnd
:function
    set foobar=1
    if "%foobar%" == "1" exit /B 1
    goto :EOF
:functionEnd

Unlike Bash, Windows batch files have no function capabilities. Clever hacks like the above can be used to fake out functions, but these hacks hide some subtle quirks. You see that exit call within the 'function'? It only gets called if the %foobar% variable is equal to 1 (which is always the case, in our example). Also note that we exit with an error code of 1. So, in short, this script should always return an exit code of 1. Now, let's create another batch script which we'll name script_b.bat:

@echo off

call script_a.bat
echo Exit Code = %ERRORLEVEL%

This second script is very simple. All we do is call script_a.bat, and then print its resulting return code. What do you expect the return code to be? One would expect it to be 1, but it's not! Our second script will actually print out Exit Code = 0. Why is this?

The answer lies in the call command. Again, unlike Bash scripts, stand-alone batch files do not create their own context when executed. But if you use the call command, the thing you call does get its own context. How weird is that? So, let's trace the first script we wrote to figure out where the error code gets changed.

After some initial setup, we call our function (call :function). Inside our function, we create a variable, initialize it to 1, then test to see if the value is 1. Since the value is indeed 1, the if test succeeds, and the exit command is called. But we don't exit the script; instead, we exit the context that was created when we called our function. Note that immediately after we call our function, we perform a cd operation. This line of code gets executed, succeeds, and sets the %ERRORLEVEL% global to 0.

In order to exit properly, we have to exit our initial script twice, like this:

@echo off
SETLOCAL

call :function
if "%ERRORLEVEL%" == "1" exit /B 1

cd %SOME_PATH%

goto :functionEnd
:function
    set foobar=1
    if "%foobar%" == "1" exit /B 1
    goto :EOF
:functionEnd

See the new exit call after our initial function call? Then, and only then, will our second script print out what we expected. This subtle behavior stymied me for several hours today; hopefully this short post will help someone else avoid this frustration.

15 Comments

kip

5:00 PM on May 23, 2008
Batch files are so crippled because they have to backwards compatible all the way back to when they were introduced in DOS 1.0, which was like 1980. That makes it very hard to introduce new features. I found this workaround recently for getting behavior like UNIX backquotes: REM similar to: set HOSTNAME=`hostname` for /f %%i in ('hostname') do set HOSTNAME=%%i

giskier

8:25 PM on May 31, 2008
Doing exit /B x from a 'function' is fine. Doing this will set the error level. There is a new full featured debugger for batch files. It runs in a Visual-Studio like environment. The product name is 'Running Steps', and it can be found in http://www.steppingsoftware.com. I find it very useful.

modbom

8:22 AM on Aug 19, 2008
Hi Jonah, I've the same problem you had. I tried your example above but unfortunately it doesn't work for me. After calling script_a.bat I allways get ERRORLEVEL=0. I'm using WinXP SP2. Do you have any idea? greetz modbom

Jonah

2:22 PM on Aug 19, 2008
If you are using the first instance of script_a (the first code block), the errorlevel will always be 0 (because the final cd call always succeeds, assuming %SOME_PATH% exists). The second form of script_a (the third code block) will produce an errorlevel of 1. I just tried it on my Windows XP SP-2 box here, and it worked just fine.

modbom

2:34 PM on Aug 19, 2008
Mmh... I tried the second one. I tried also a batch script foo.bat only containing:
@echo off
exit /b 1
After modifying script_b.bat to:
@echo off

call foo.bat
echo Exit Code = %ERRORLEVEL%
and running them, the output is Exit Code = 0. It's very curious. Nevertheless thanks a lot for your quick response.

Jonah

3:42 PM on Aug 19, 2008
I'm not sure why the above examples don't work for you. You might take a look at the documentation in your command prompt for the exit command: exit /? The set command might also be useful: set /? Also, remove the echo @off line (so that all the lines of the script are echoed). That would help you figure out what the last issued command is. Other than that, I can't think of why it wouldn't work for you. The above works just fine on my Windows XP SP2 box.

kip

5:01 PM on Aug 19, 2008
modbom- not sure but maybe this is related? Try checking the registry settings mentioned there.

modbom

7:39 AM on Aug 21, 2008
Thx for our replies. No it works, but i'm not unsure why. I thinks it was caused by a side effect of another script called long before the above mentioned. Something like set %ERRORLEVEL%=0 But now it works. Thx!

colinnwn

10:50 PM on Jun 17, 2009
I tried all suggestions here and didn't work for me, but I changed my exit statement from EXIT /B 1 to just EXIT 1 And even without the double exit function call method, I started getting the correct exit code. Moral of the story is the /B may be inappropriate use of a switch.

Jonah

2:07 AM on Jun 18, 2009
It may depend on what version of Windows you are writing for. Here's the exit command help from Windows XP:
Quits the CMD.EXE program (command interpreter) or the current batch
script.

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.

colinnwn

9:51 PM on Jun 25, 2009
Sorry Jonah, I should have been more concise. I am using Windows XP SP3. exit /? returns the following INACCURATE information. Microsoft Windows XP [Version 5.1.2600] (C) Copyright 1985-2001 Microsoft Corp. F:\>exit /? Quits the CMD.EXE program (command interpreter) or the current batch script. 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.

Jonah

12:08 AM on Jun 26, 2009
The /B parameter works for me in Windows XP SP3. Not sure what might be wrong...

crem88

4:14 PM on Mar 4, 2010
Jonah, Good post. Although I disagree that this is a "subtle bug". Maybe a subtle behavior. The behavior is what is to be expected. Using "Call" within a batch file creates a new batch context - whether you are calling another batch file or a label. "By using call with this parameter, you create a new batch file context and pass control to the statement after the specified label" http://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/if.mspx?mfr=true Exit /b "Exits the current batch script." So when you "exit /b 1" in the function block, you are exiting the batch context of the label. You are returning from the function block. What you do after the return is up to you. A real limiting aspect faking functions is that you can pass values to the function block, but you cannot pass them back. You can get around that somewhat using global variables (passing the name (reference) of the variable as opposed the value).

Jonah

4:18 PM on Mar 4, 2010
You are correct in that it's more of a subtle behavior. If batch files supported functions, this wouldn't be a problem. Batch makes bash look great ... and bash sucks!

Zack

3:57 PM on Jul 21, 2010
Worked for me. Thanks a lot and this is what I was looking for.

Leave a Comment

Ignore this field:
Never displayed
Leave this blank:
Optional; will not be indexed
Ignore this field:
Both Markdown and a limited set of HTML tags are supported
Leave this empty: