OK - I think I have completed the exception handling to my satisfiaction.
I discovered a bug in my previous version - The unhandled exception message was supposed to report what batch script failed to handle the exception, but instead it was always reporting exception.bat. I've fixed that silly bug in my new code.
Note that the reported exception.loc property is the location where the exception was
thrown, not where the exception is caught.
One other point of note - the code between
%@Try% and
%@EndTry% is within a block, so delayed expansion or CALL will have to be used to access variables defined within the block. The parentheses are part of the macro definitions for convenience. But the block is not obvious, so you may want to add additional parentheses to make the block visible:
Code: Select all
(%@Try%
REM code that may raise an exception goes here
%@EndTry%)
:@Catch
REM code that handles any exception goes here
:@EndCatch
One of the things I wanted was to be able to propagate an exception across script CALLs (when a script returns with an unhandled exception). I'm not able to do it directly, but I solved the problem by storing the exception properties using DOSKEY macros. I then added an exception load routine to retrieve the exception from DOSKEY.
The DOSKEY commands take a significant amount of time, so the EXCEPTION THROW command only saves the unhandled exception definition with DOSKEY if variable EXCEPTION.PRESERVE is defined.
Also in the interest of performance, I have adopted a convention that exceptions should return negative ERRORLEVEL. This enables my code to avoid unnecessary checks for an unhandled exception upon CALL return. But that convention is not a requirement of exception.bat.
Unfortunately, the CALL must have stderr redirected to NUL if you don't want to see the unhandled exception message from the CALLed script. Of course you would probably not redirect stderr to NUL if you choose not to propagate the exception.
Here is an example of how to call a script and propagate any returned exception, assuming the exception ERRORLEVEL is negative:
Code: Select all
call SomeScriptThatMayReturnAnException 2>nul || if not errorlevel 0 call load exception
if defined exception.code call exception rethrow %%exception.code%% "%%exception.message%%" "%%exception.loc%%"
The CALL with exception detection certainly can be within a TRY block, but it need not be. I have assumed the CALL is within some type of block, hence the percent doubling in the rethrow.
If you opt not to use negative ERRORLVEL for exceptions, then simply remove the IF NOT ERRORLEVEL 0 from the CALL code.
So here is my "final"
exception.bat utility script:
Code: Select all
@echo off
shift /1 & goto %1
:throw errCode errMsg errLoc [/M]
set "exception.Stack="
:: Fall through to :rethrow
:rethrow errCode errMsg errLoc [/M]
setlocal disableDelayedExpansion
setlocal enableDelayedExpansion
set "exception.Stack=[%~1:%~2] !exception.Stack!"
(
for /l %%# in (1 1 10) do for /f "delims=" %%S in (" !exception.Stack!") do (
(goto) 2>NUL
setlocal disableDelayedExpansion
call set "funcName=%%0"
call set "batName=%%~f0"
setlocal EnableDelayedExpansion
set "exception.Stack=!funcName!%%S"
if !exception.Try! == !funcName! (
endlocal
endlocal
set "exception.Code=%~1"
set "exception.Msg=%~2"
set "exception.Loc=%~3"
set "exception.Stack=%%S"
set "exception.Try="
(CALL )
goto :@Catch
)
if "!funcName:~0,1!" neq ":" (
echo(
echo Unhandled batch exception:
if "%~1" neq "" for /f "delims=" %%M in ("%~1") do (
echo Code = %%M
if defined exception.Preserve doskey /exename=exception exception.Code=%%M
)
if "%~2" neq "" for /f "delims=" %%M in ("%~2") do (
echo Msg = %%M
if defined exception.Preserve doskey /exename=exception exception.Msg=%%M
)
echo Bat = !batName!
if defined exception.Preserve doskey /exename=exception exception.Bat=!batName!
if "%~3" neq "" for /f "delims=" %%M in ("%~3") do (
echo Loc = %%M
if defined exception.Preserve doskey /exename=exception exception.Loc=%%M
)
echo Stack: !funcName!%%S
if defined exception.Preserve doskey /exename=exception exception.Stack=!funcName!%%S
exit /b %~1
) >&2
)
call "%~f0" rethrow %1 %2 %3
)
:: Never reaches here
:init
set "@Try=(call set exception.Try=%%0"
set "@EndTry=set "exception.Try=" & goto :@endCatch)"
set "exception.Try="
call :clear
exit /b
:load
for /f "delims=" %%A in ('doskey /m:exception') do set "%%A"
exit /b
:clear
for %%A in (Code Msg Loc Stack Bat) do set "exception.%%A="
for /f "delims==" %%A in ('doskey /m:exception') do doskey /exename=exception %%A=
exit /b
And below is script to test the capabilities. The script recursively calls itself 7 times. Each iteration has two CALLs, one to a :label that demonstrates normal exception propagation, and the other to a script that demonstrates exception propagation across script CALLs.
While returning from a recursive call, it throws an exception if the iteration count is a multiple of 3 (iterations 3 and 6).
Each CALL has its own exception handler that normally reports the exception and then rethrows a modified exception. But if the iteration count is 5, then the exception is handled. Note that the exception should be cleared if it is handled (not thrown or rethrown).
testException.batCode: Select all
@echo off
:: Main
setlocal disableDelayedExpansion
if not defined @Try (
call exception init
set "exception.preserve=1" This is needed to propagate exceptions accross script calls
)
set /a cnt+=1
echo Main Iteration %cnt% - Calling :Sub
%@Try%
call :Sub
call echo Main Iteration %cnt% - :Sub returned %%errorlevel%%
%@EndTry%
:@Catch
echo(
echo Main Iteration %cnt% - Exception detected:
echo Code = %exception.code%
echo Message = %exception.msg%
echo Location = %exception.loc%
echo Rethrowing modified exception
echo(
call exception rethrow -%cnt% "Main Exception" "%~f0(%0)"
:@EndCatch
echo Main Iteration %cnt% - Exit
exit /b %cnt%
:Sub
setlocal
echo :Sub Iteration %cnt% - Start
(%@Try%
if %cnt% lss 7 (
echo :Sub Iteration %cnt% - Calling testException.bat
%= The next two lines show how to call a script and propagate any returned exception. =%
%= I redirect stderr to nul to hide the "unhandled exception" message from the CALLed script. =%
%= I use a convention that negative ERRORLEVEL implies an exception so I don't waste time =%
%= trying to load an exception that isn't there. =%
call testException 2>nul || if not errorlevel 0 call exception load
if defined exception.code call exception rethrow %%exception.code%% "%%exception.msg%%" "%%exception.loc%%"
%= Show any non-exception return code (demonstrate ERRORLEVEL is preserved if no exception) =%
call echo :Sub Iteration %cnt% - testException returned %%errorlevel%%
)
%= Throw an exception if the iteration count is a multiple of 3 =%
set /a "1/(cnt%%3)" 2>nul || (
echo Throwing exception
call exception throw -%cnt% "Divide by 0 exception" "%~f0(%0)"
)
%@EndTry%)
:@Catch
echo(
echo :Sub Iteration %cnt% - Exception detected:
echo Code = %exception.code%
echo Message = %exception.msg%
echo Location = %exception.loc%
%= Handle the exception if iteration count is a multiple of 5, else rethrow it with new properties =%
set /a "1/(cnt%%5)" 2>nul && (
echo Rethrowing modified exception
echo(
call exception rethrow -%cnt% ":Sub Exception" "%~f0(%0)"
) || (
call exception clear
echo Exception handled
echo(
)
:@EndCatch
echo :Sub Iteration %cnt% - Exit
exit /b %cnt%
Here is output of a test run
Code: Select all
C:\test>testException
Main Iteration 1 - Calling :Sub
:Sub Iteration 1 - Start
:Sub Iteration 1 - Calling testException.bat
Main Iteration 2 - Calling :Sub
:Sub Iteration 2 - Start
:Sub Iteration 2 - Calling testException.bat
Main Iteration 3 - Calling :Sub
:Sub Iteration 3 - Start
:Sub Iteration 3 - Calling testException.bat
Main Iteration 4 - Calling :Sub
:Sub Iteration 4 - Start
:Sub Iteration 4 - Calling testException.bat
Main Iteration 5 - Calling :Sub
:Sub Iteration 5 - Start
:Sub Iteration 5 - Calling testException.bat
Main Iteration 6 - Calling :Sub
:Sub Iteration 6 - Start
:Sub Iteration 6 - Calling testException.bat
Main Iteration 7 - Calling :Sub
:Sub Iteration 7 - Start
:Sub Iteration 7 - Exit
Main Iteration 7 - :Sub returned 7
Main Iteration 7 - Exit
:Sub Iteration 6 - testException returned 7
Throwing exception
:Sub Iteration 6 - Exception detected:
Code = -6
Message = Divide by 0 exception
Location = C:\test\testException.bat(:Sub)
Rethrowing modified exception
Main Iteration 6 - Exception detected:
Code = -6
Message = :Sub Exception
Location = C:\test\testException.bat(:Sub)
Rethrowing modified exception
:Sub Iteration 5 - Exception detected:
Code = -6
Message = Main Exception
Location = C:\test\testException.bat(testException)
Exception handled
:Sub Iteration 5 - Exit
Main Iteration 5 - :Sub returned 5
Main Iteration 5 - Exit
:Sub Iteration 4 - testException returned 5
:Sub Iteration 4 - Exit
Main Iteration 4 - :Sub returned 4
Main Iteration 4 - Exit
:Sub Iteration 3 - testException returned 4
Throwing exception
:Sub Iteration 3 - Exception detected:
Code = -3
Message = Divide by 0 exception
Location = C:\test\testException.bat(:Sub)
Rethrowing modified exception
Main Iteration 3 - Exception detected:
Code = -3
Message = :Sub Exception
Location = C:\test\testException.bat(:Sub)
Rethrowing modified exception
:Sub Iteration 2 - Exception detected:
Code = -3
Message = Main Exception
Location = C:\test\testException.bat(testException)
Rethrowing modified exception
Main Iteration 2 - Exception detected:
Code = -2
Message = :Sub Exception
Location = C:\test\testException.bat(:Sub)
Rethrowing modified exception
:Sub Iteration 1 - Exception detected:
Code = -2
Message = Main Exception
Location = C:\test\testException.bat(testException)
Rethrowing modified exception
Main Iteration 1 - Exception detected:
Code = -1
Message = :Sub Exception
Location = C:\test\testException.bat(:Sub)
Rethrowing modified exception
Unhandled batch exception:
Code = -1
Msg = Main Exception
Bat = C:\test\testException.bat
Loc = C:\test\testException.bat(testException)
Stack: testException [-1:Main Exception] :Sub [-1::Sub Exception] [-2:Main Exception] testException [-2:Main Exception] :Sub [-2::Sub Exception] [-3:Main Exception] testException [-3:Main Exception] :Sub [-3::Sub Exception] [-3:Divide by 0 exception]
Upon return, the exception is not defined in any variable, but the exception properties are stored within DOSKEY macros:
Code: Select all
C:\test>set exception
Environment variable exception not defined
C:\test>doskey /m:exception
exception.Stack=testException [-1:Main Exception] :Sub [-1::Sub Exception] [-2:Main Exception] testException [-2:Main Exception] :Sub [-2::Sub Exception] [-3:Main Exception] testException [-3:Main Exception] :Sub [-3::Sub Exception] [-3:Divide by 0 exception]
exception.Loc=C:\test\testException.bat(testException)
exception.Bat=C:\test\testException.bat
exception.Msg=Main Exception
exception.Code=-1
You can retrieve the exception via the EXCEPTION LOAD command:
Code: Select all
C:\test>exception load
C:\test>set exception
exception.Bat=C:\test\testException.bat
exception.Code=-1
exception.Loc=C:\test\testException.bat(testException)
exception.Msg=Main Exception
exception.Stack=testException [-1:Main Exception] :Sub [-1::Sub Exception] [-2:Main Exception] testException [-2:Main Exception] :Sub [-2::Sub Exception] [-3:Main Exception] testException [-3:Main Exception] :Sub [-3::Sub Exception] [-3:Divide by 0 exception]
You can clear the exception via EXCEPTION CLEAR
Code: Select all
C:\test>exception clear
C:\test>set exception
Environment variable exception not defined
C:\test>doskey /m:exception
C:\test>
If you want to include line numbers within your messages, then you can use any of the methods described at
viewtopic.php?f=3&t=6455.
My favorite is my latest JREPL.BAT line numbering method at
viewtopic.php?p=41762#p41762.
Dave Benham