I must admit that I don’t much care for PowerShell’s default behaviour with respect to errors, which is to continue on error. It feels very VB6 “On Error Resume Next”-ish. Given that it is a shell scripting language, I can understand why the PowerShell team chose this as a default. Fortunately you can change the default by setting $ErrorActionPreference = ‘Stop’, which terminates execution by throwing an exception. (The default value is Continue, which means the script prints the error and continues executing.) Unfortunately this only works for PowerShell commands and not external executables that return non-zero error codes. (In the shell world, a return code of zero (0) indicates success and anything else indicates failure.)
Take the following simple script:
'Starting script...' $ErrorActionPreference = 'Stop' ping -badoption "Last Exit Code was: $LastExitCode" rm nonexistent.txt 'Finished script'
Notice how execution continued after the ping command failed with an exit code of one (1) even though we have $ErrorActionPreference set to ‘Stop’. Also notice that the rm command, which is an alias for the PowerShell command, Remove-Item, did cause execution to abort as expected and ‘Finished script’ was never printed to the console. The discrepancy in error handling between PowerShell commands and executables is annoying and forces us to constantly think about what we’re calling – a PowerShell command or an executable. The obvious solution is:
'Starting script...' $ErrorActionPreference = 'Stop' ping -badoption if ($LastExitCode -ne 0) { throw 'An error has occurred...' } rm nonexistent.txt 'Finished script'
The error handling code adds a lot of noise, IMHO, and feels like a throwback to COM and HRESULTs. Can we do better? Jorge Matos, one of the psake contributors came up with this elegant helper function:
function Exec([scriptblock]$cmd, [string]$errorMessage = "Error executing command: " + $cmd) { & $cmd if ($LastExitCode -ne 0) { throw $errorMessage } }
Note the “& $cmd” syntax. $cmd is a scriptblock and & is used to execute the scriptblock. We can now re-write our original script as follows. (N.B. Exec function is elided for brevity.)
'Starting script...' $ErrorActionPreference = 'Stop' exec { ping -badoption } rm nonexistent.txt 'Finished script'
The script now terminates when the bad ping command is executed. We do have to remember to surround executables with exec {}, but this is less noise IMHO than having to check $LastExitCode and throwing an exception.
For those of you using psake for your builds, the Exec helper function is included in the latest versions of the psake module. So you can use it in your build tasks to ensure that you don’t try to run unit tests if msbuild fails horribly.
Happy Scripting!