Skip to main content

How to Return a Value from a Subroutine in Batch Script

Subroutines (or "functions") are essential for writing clean, reusable, and organized batch scripts. You create them with a label (:MySubroutine) and execute them with CALL. A key part of any function is its ability to perform a calculation or an operation and then return a result to the main script. However, unlike most programming languages, batch scripting has no return statement.

Furthermore, the best practice of using SETLOCAL inside a subroutine to prevent variables from "leaking" creates a scope barrier: any variable you create is destroyed when the subroutine ends with ENDLOCAL. This guide will teach you the standard, professional method for safely passing a value out of a subroutine's local scope and back to the main caller.

The Challenge: SETLOCAL and Variable Scope

Good script design dictates that you use SETLOCAL at the start of a subroutine. This creates a new, local environment. Any variables you create or modify will not affect the rest of the script. When ENDLOCAL is called (or the script ends), this local environment is destroyed, along with all its variables.

This creates a problem: how do you get a result out?

An example of the problem

@ECHO OFF
SET "MyResult="
ECHO Before call, MyResult is: "%MyResult%"

CALL :CalculateValue

ECHO After call, MyResult is: "%MyResult%"
GOTO :EOF

:CalculateValue
SETLOCAL
SET "LocalValue=This is the result"
ECHO Inside subroutine, LocalValue is: "%LocalValue%"
ENDLOCAL
GOTO :EOF

Output:

Before call, MyResult is: ""
Inside subroutine, LocalValue is: "This is the result"
After call, MyResult is: ""

The LocalValue was successfully created but was trapped and destroyed by ENDLOCAL. We need a way to pass it across that barrier.

The most robust and widely accepted method is to pass the name of the variable you want the result to be stored in as an argument to the subroutine. The subroutine then uses a special trick to set this variable in the caller's scope as it exits.

@ECHO OFF
SETLOCAL

SET "MyResult="
ECHO Before call, MyResult is: "%MyResult%"

REM Call the subroutine and tell it to put the result in a variable named "MyResult"
CALL :CalculateValue MyResult

ECHO After call, MyResult is: "%MyResult%"
GOTO :EOF


:CalculateValue
SETLOCAL
REM %1 is the first argument passed to the subroutine, which is "MyResult"
SET "LocalResult=This value came from the subroutine"

REM --- This is the magic line ---
ENDLOCAL & SET "%1=%LocalResult%"
GOTO :EOF

Output:

Before call, MyResult is: ""
After call, MyResult is: "This value came from the subroutine"

Success! The MyResult variable in the main script was successfully updated.

How the ENDLOCAL & SET Trick Works

The line ENDLOCAL & SET "%1=%LocalResult%" is one of the most clever and important idioms in advanced batch scripting. It works because of the order in which the command processor parses the line.

  1. & is a command separator, which allows two or more commands on a single logical line.
  2. Before any command on the line is executed, the command processor parses the entire line and expands all %PERCENT% variables.
  3. During this parsing phase, %1 is replaced with MyResult and %LocalResult% is replaced with This value came from the subroutine.
  4. The command that is now ready to be executed looks like this in memory: ENDLOCAL & SET "MyResult=This value came from the subroutine"
  5. Now, the commands are executed. First, ENDLOCAL runs, destroying the local scope and the LocalResult variable.
  6. Next, the second command, SET "MyResult=This value came from the subroutine", is executed. Since ENDLOCAL has already finished, this SET command is running in the caller's scope, where it correctly assigns the value to the MyResult variable.

Method 2 (Simple but Risky): Using Global Variables

A much simpler, but less safe, way to return a value is to simply not use SETLOCAL. Any variable you create or modify inside the subroutine will be "global" and will persist after it finishes.

For example:

@ECHO OFF
CALL :SetGlobalValue
ECHO The global value is: %GlobalVar%
GOTO :EOF

:SetGlobalValue
SET "GlobalVar=This is visible everywhere"
GOTO :EOF
warning

Why is this risky? This approach can lead to "side effects." If your main script also uses a variable named GlobalVar for something else, the subroutine will overwrite it, potentially causing bugs that are very difficult to track down. Using SETLOCAL and the "by reference" method prevents these kinds of conflicts and is a much better practice for writing large, complex scripts.

Common Pitfalls and How to Solve Them

  • Special Characters: The ENDLOCAL & SET trick is very robust, but strings with special characters can still be tricky. Always quote the SET assignment (SET "%1=%Value%").
  • Forgetting GOTO :EOF: Every subroutine must end with GOTO :EOF. If you forget it, the script will "fall through" and continue executing the code below it, leading to unexpected behavior.

Practical Example: A Subroutine to Add Two Numbers

This script demonstrates a function-like subroutine that takes two numbers as input and "returns" their sum to a variable specified by the caller.

@ECHO OFF
SETLOCAL

CALL :AddNumbers Sum 10 5
ECHO The sum of 10 and 5 is: %Sum%

CALL :AddNumbers AnotherSum 100 23
ECHO The sum of 100 and 23 is: %AnotherSum%

GOTO :EOF


:AddNumbers
REM Usage: CALL :AddNumbers <ReturnVar> <Number1> <Number2>
SETLOCAL
SET "Num1=%2"
SET "Num2=%3"
SET /A "Result=%Num1% + %Num2%"

ENDLOCAL & SET "%1=%Result%"
GOTO :EOF

Conclusion

While batch scripting doesn't have a formal return statement, it provides a powerful mechanism to achieve the same result safely and effectively.

  • The "return by reference" method is the professional and recommended best practice. It uses SETLOCAL for safe, local scoping and the ENDLOCAL & SET "%1=%Value%" trick to pass the result back to the caller.
  • The global variable method is simpler but should be avoided in complex scripts, as it can lead to unintended side effects and bugs.

By mastering the ENDLOCAL & SET idiom, you can write clean, modular, and reusable subroutines that behave like functions in any other programming language.