Tuesday 3 May 2011

PowerShell, Throwing Exceptions & Exit Codes

[I raised a cut down version of this as a question via a comment to a related post on the Windows PowerShell blog back in March. I’ve not seen a response yet and doubt I ever will as it’s an old post and so I very much doubt anyone is monitoring it.]

I’ve got a bit of a love/hate relationship with PowerShell at the moment. Naturally whilst learning any new language the books steer you nicely towards the things that work, but as you start to “do your own thing” you step outside that comfort zone and the warts and inconsistencies start to appear. I should point out that this particular affliction affects more than just PowerShell[*] but it’s worse because it appears inconsistent in its behaviour and so appears to work – sometimes.

The Process Exit Code

Every process can return an exit code to signal to its caller something about the outcome of the task it was asked to perform. I don’t believe there is a formal definition anywhere about what constitutes “success” and “failure”[#] but the established convention is that zero means success and non-zero means unsuccessful. Of course what “unsuccessful” then means opens a whole new can of worms but if you’re writing a Windows batch file then the following construct is probably embedded in your head:-

<execute some process>
IF ERRORLEVEL 1 (
  ECHO ERROR: <some error message>
  EXIT /B 1
)

The ERRORLEVEL test is read as “if the last process exit code was greater than or equal to 1”. This assumes that the exit code will always be positive which is pretty much the norm, but it doesn’t have to be. So what does this have to do with PowerShell then?

The PowerShell EXIT & THROW Keywords

If you transliterate a Windows batch file into PowerShell you will probably end up writing code like this:-

<do something>
if ( <not some condition> )
{
  write-output “something bad occurred”
  exit 1
}

This is because “echo” is aliased as “write-output” and PowerShell still has an equivalent “exit” keyword to terminate the script[+]. So far so good, but PowerShell can do so much more and it also supports a “throw” keyword to allow you to use a more modern style of exception handling in your code that is especially useful when combined with functions. So you might expect that you could avoid the hard-coded exit code and use something like this instead (which is what I wanted to do):-

<do something>
if ( <not some condition> )
{
  throw “something bad occurred”
}

Yes, I know that I’m using exceptions for error handling and that might be considered bad form, but in the places I was using this idiom the errors were truly non-recoverable and so the effect would be the same – the script should terminate and the caller be signalled that a fatal error occurred.

Unhandled Exceptions

The way that I test all my scripts and processes to ensure that they exit with a well formed result code is by using the following Windows batch file, which I name “RUN.CMD”:-

@ECHO OFF
CALL %*
ECHO.
ECHO ExitCode=[%ERRORLEVEL%]

So I ran my PowerShell script to test the error handling and I noticed that it always returned 0, irrespective of whether it terminated with a throw or not. The output showed the details of the exception as I expected, but with the exit code being 0 my calling parent batch scripts and job scheduler would not be able to detect a failure (without some really ugly scraping of the output streams). So I tried a few experiments with the throw construct. First an ‘inline’ command:-

C:\Temp>run PowerShell -command "throw 'my error'"
my error
At line:1 char:6
+ throw <<<<  'my error'
    + CategoryInfo : OperationStopped: (my error:String) [], RuntimeException
    + FullyQualifiedErrorId : my error

ExitCode=[1]

Great, an unhandled exception in a inline script causes PowerShell.exe to return a non-zero exit code. What about if I put the same one-liner in a .ps1 script file and execute it:-

C:\Temp>run PowerShell -file test.ps1
my error
At C:\Temp\test.ps1:1 char:6
+ throw <<<<  'my error'
    + CategoryInfo : OperationStopped: (my error:String) [], RuntimeException
    + FullyQualifiedErrorId : my error

ExitCode=[0]

Not so good. Yes we get the error message, but PowerShell.exe exited with a code that signals success. I have always specified the –File switch when running a script to avoid the need to do the whole .\ relative path thing. So what about running the same script file as a Command, surely that would be the same, wouldn’t it?

C:\Temp>run PowerShell -command .\test.ps1
my error
At C:\Temp\test.ps1:1 char:6
+ throw <<<<  'my error'
    + CategoryInfo : OperationStopped: (my error:String) [], RuntimeException
    + FullyQualifiedErrorId : my error

ExitCode=[1]

Meh? So, depending on whether I use the “–File” or “–Command” switch to execute the .ps1 script I get different behaviour. Is this a PowerShell bug or is there something fundamental about the execution model the differs between –File and –Command that I’ve yet to understand? Either way I wouldn’t want to rely on someone not being helpful and “fixing” the command line by switching the use of –Command to –File, especially as it affects error handling and we all know how hard people test their changes to verify the error handling still works as designed…

Trap To The Rescue

I have found a somewhat invasive workaround that at least ensures a sensible exit code at the expense of less pretty output. It relies on adding a Trap handler at the top of the script to catch all errors, output them and then manually exit the script:-

trap
{
  write-output $_
  exit 1
}

throw 'my error'

Here’s the output from it when run with the previously unhelpful “-File” switch:-

C:\Temp>run PowerShell -file test.ps1
my error
At C:\Temp\test.ps1:7 char:6
+ throw <<<<  'my error'
    + CategoryInfo : OperationStopped: (my error:String) [], RuntimeException
    + FullyQualifiedErrorId : my error

ExitCode=[1]

Now that’s better. The obvious change would be to use Write-Error as the output cmdlet but that has a side-effect when using redirection[+]. There is probably some way I can have my cake and eat it but my PowerShell skills are less than stellar at the moment and my Googling has turned up nothing positive so far either.

 

[*] Matthew Wilson wrote a Quality Matters column for one of the ACCU journals where he showed that C++, Java & C# all treated an unhandled exception in main() as a “successful” execution as far as reporting the process result code goes.

[#] In C/C++ you can use the two constants EXIT_SUCCESS & EXIT_FAILURE to avoid hard-coding a return code that is platform dependent. On Windows these equate to 0 and 1 respectively, although the latter could be any non-zero value. I seem to recall that these constants are defined by <stdlib.h>.

[+] Let’s ignore the fact that you might use “write-error” and are keeping to a similar model to cmd.exe. I have another post queued up that shows that the output mechanism is annoyingly broken in PowerShell when using file redirection if you’re considering using it to replace batch files that run under, say, a job scheduler.

15 comments:

  1. It's even worse than that. If your script has a param definition that uses the Parameter attribute on any of the parameters, the trap trick doesn't even work.

    Example:

    param(
    [Parameter(Position=0,Mandatory=0)]
    [string]$aParam
    )

    exit 1

    That will exit with code 0. Removing the Parameter attribute causes it to have an exit code of 1:

    param(
    [Parameter(Position=0,Mandatory=0)]
    [string]$aParam
    )

    exit 1

    Using [alias] seems fine.

    ReplyDelete
  2. Looks like something happened my commenting hash in the second script. I meant this:

    param(
    [string]$aParam
    )

    exit 1

    ReplyDelete
  3. "but as you start to “do your own thing” you step outside that comfort zone and the warts and inconsistencies start to appear"
    And too often, doing your own thing means getting basic things like this to work - I feel your frustration.

    ReplyDelete
  4. Came across this post whilst searching on the topic. Later I happend upon a blog post which appears address the issue of obtaining PS exit codes for use in a calling batch file for error checking.

    http://thepowershellguy.com/blogs/posh/archive/2008/05/20/hey-powershell-guy-how-can-i-run-a-powershell-script-from-cmd-exe-and-return-an-errorlevel.aspx

    ReplyDelete
  5. They need to add a switch -ExceptionExitCode to the PowerShell.exe itself so we can say, return code x when a script exits due to an exception. I expect MS couldn't decide on a suitable exit code to set by default and avoided implementing it, knowing we can code around it.

    ReplyDelete
  6. Too funny - Fancy find you here Mr Puplett, long time no speak. It's Brendan from SocGen, your former scripting protégé from down under... I was just investigating this very issue myself when I saw your post.

    In the end I think I will settle on the following solution using .Net as it seems to cover most bases... but unfortunately causes PowerGUI to quit without warning which is a pain :(

    trap
    {
    [Environment]::Exit("$errorcode")
    }
    $errorcode = 1
    throw "my error"

    Found here:
    http://www.maxtblog.com/2011/09/creating-your-own-exitcode-in-powershell-and-use-it-in-ssis-package/

    ReplyDelete
  7. In your first PS-Example, you just execute a command, which gives a return code of 1. In the second example you call a script. Inside the script, the throw gives a return code of 1, but the script itself has a return code of 0.
    Try with a custom build subroutine inside your script. If you write 'exit 1', it leaves the subroutine and in your script you can use the result code. When your script ends, you have to write the result code for your script or just be fine with your result code 0.

    ReplyDelete
    Replies
    1. Bullshit! A script with CmdLetBindings and parameters called from a batch must return its exit code to the batch - the caller.

      Delete
  8. Thank you for the post it helped me fixed the Count error for me

    ReplyDelete
  9. I just hate powershell in the meanwhile.

    Just for the record:
    You cannot use 'exit' in the trap if your script uses the Param block WITH the Parameter-Attribute. If you do, every exit code is ignored and the parent receives always exit code 0. Wonderful. Can somebody tell me why?

    Now, you can use [Environment]::Exit(1) instead.

    Well, however if you replace the Write-Output in the trap with Write-Error as somebody might want to have the nice syntax coloring, you are again in the powers-hell.
    Because if your script uses $ErrorActionPreference = "stop" to ensure errors do not continue and are caught by the trap, then you are again out of luck. Write-Error's behaviour then changes from non-terminating to terminating. So any exit code in the trap after that returns 0 again. Great!

    All those combinations of side effects make powershell so damn complex.

    ReplyDelete
    Replies
    1. I use this to report error nicely:
      Write-Host "$($_.Exception.Message)" -ForegroundColor Red

      Delete
  10. So, some 5 years later I finally unearthed a KB article by Microsoft describing the problem. It appears to have been fixed in PowerShell v3.0.

    "Incorrect error code when you run a PowerShell script at a command prompt in Windows 7 or in Windows Server 2008 R2"

    https://support.microsoft.com/en-us/kb/2552055

    NOTE: If you use the PowerShell -Version switch to run under an older engine it will exhibit the buggy behaviour (as you would expect).

    ReplyDelete
  11. I've now added a Gist with a test script to make it easy to reproduce and verify whether you have this problem:

    https://gist.github.com/chrisoldwood/bbd6aac19bac2ff0c54ba32a705c939b

    (It's only questionable if you are using a version of PowerShell <= 2.0)

    ReplyDelete
  12. Hi Chris
    Thanks for writing about the pain. I have another one. If you use -Command you need to escape brackets. If you use -File you do not need to :)

    ReplyDelete