Skip to main content

PowerShell Error Handling Behavior Debunked

Note: I am using simple error messages as an example, please reference the best practices and guidelines I outlined on when to use custom error messages.

I have been churning in my mind for the last few days all the entries in the 2013 Scripting Games and how they handle errors, or lack thereof.

I am coming to the conclusion through some testing that the simple fact of seeing a try..catch or throw statements does not mean there is proper error handling.

I've been testing several variations and forms of error handling, so lets start with the basics.
  1. function Test-WriteError {  
  2.     [CmdletBinding()] param()  
  3.   
  4.     "Test-WriteError::ErrorActionPreference = $ErrorActionPreference"  
  5.     Move-Item -Path 'C:\Does\Not\Exists.log' -Destination 'C:\No\Where'     
  6.     "Test-WriteError::End"  
  7. }  
Test-WriteError::ErrorActionPreference = Continue
Move-Item : Cannot find path 'C:\Does\Not\Exists.log' because it does not exist.
At line:6 char:5
+     Move-Item -Path 'C:\Does\Not\Exists.log' -Destination 'C:\No\Where'
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\Does\Not\Exists.log:String) [Move-Item], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.MoveItemCommand

Test-WriteError::End

This is no error handling, and you can see what happens, it hits the error and blows on through, Write-Error is for a non-terminating error. Perfect, but not very friendly from a user perspective, e.g. the error talked about Move-Item but I called Test-WriteError, why? Well let's add our own custom error handling.

Try..Catch vs. ErrorAction+ErrorVariable
I'm not going to solve this, either form will work as long as you are aware of the differences in how to utilize the error handling appropriately.

Try..Catch
  1. function Test-WriteError {  
  2.     [CmdletBinding()] param()  
  3.   
  4.     "Test-WriteError::ErrorActionPreference = $ErrorActionPreference"  
  5.   
  6.     try {  
  7.         Move-Item -Path 'C:\Does\Not\Exists.log' -Destination 'C:\No\Where' -ErrorAction Stop  
  8.     } catch {  
  9.         Write-Error $_.Exception.Message  
  10.     }  
  11.   
  12.     "Test-WriteError::End"  
  13. }  
ErrorAction + ErrorVariable
  1. function Test-WriteError {  
  2.     [CmdletBinding()] param()  
  3.   
  4.     "Test-WriteError::ErrorActionPreference = $ErrorActionPreference"  
  5.     Move-Item -Path 'C:\Does\Not\Exists.log' -Destination 'C:\No\Where' -ErrorAction SilentlyContinue -ErrorVariable ex  
  6.   
  7.     if ($ex) {  
  8.         Write-Error $ex[0].Exception.Message  
  9.     }  
  10.   
  11.     "Test-WriteError::End"  
  12. }  
The -ErrorAction parameter is the critical piece of both types.

For throw, if the cmdlet is not producing a terminating error, e.g. uses Write-Error instead of throw, then catch will never trigger unless -ErrorAction Stop is included. Do not change $ErrorActionPreference, use the -ErrorAction parameter.

For the -ErrorVariable parameter, -ErrorAction should be set to SilentlyContinue so you avoid error output from the cmdlet being invoked so you can use the information to produce your own error. Also note that the ErrorVariable will always be populated, it may contain one or more ErrorRecord objects.

I personally like, try..catch; but as long as there is error handling and it works that's what is important.

Non-Terminating Errors
These are the simplest  always use Write-Error. If the -ErrorAction parameter is set to Stop PowerShell will take care of generating a terminating error exception for the caller. But remember, they are non-terminating, the code will continue to process in your script.

Terminating Errors
This is when things get fun as the behavior can be let's say, unexpected. Let's build up some of the scenarios that can lead to frustration. This code displays 3 things under normal conditions, the $ErrorActionPreference value, an custom error messages based on an error originating from the Move-Item cmdlet and then finally a string to indicate the script is still processing.
  1. function Test-WriteError {  
  2.     [CmdletBinding()] param()  
  3.   
  4.     "Test-WriteError::ErrorActionPreference = $ErrorActionPreference"  
  5.   
  6.     try {  
  7.         Move-Item -Path 'C:\Does\Not\Exists.log' -Destination 'C:\No\Where' -ErrorAction Stop  
  8.     } catch {  
  9.         throw $_.Exception.Message  
  10.     }  
  11.   
  12.     "Test-WriteError::End"  
  13. }  
Invoking Test-WriteError just as it is produces the following output:

Test-WriteError::ErrorActionPreference = Continue
Cannot find path 'C:\Does\Not\Exists.log' because it does not exist.
At line:9 char:9
+         throw $_.Exception.Message
+         ~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (Cannot find pat...does not exist.:String) [], RuntimeException
    + FullyQualifiedErrorId : Cannot find path 'C:\Does\Not\Exists.log' because it does not exist.

What's wrong with that you might be asking, the Test-WriteError::End string is missing as it should be because we caused a terminating error by using throw.

What if the user decides to modify the error behavior with the -ErrorAction parameter or worse yet they changed the $ErrorActionPreference variable?

Continue
This is the default, so that is what you see above.

Stop
This produces the same results as continue because it already is a terminating error.

SilentlyContinue
Any guessing in what this produces? Lets give it a try.

Test-WriteError -ErrorAction SilentlyContinue
Test-WriteError::ErrorActionPreference = SilentlyContinue
Test-WriteError::End

All went according to plan except our throw statement didn't terminate the running of the script. Wait, what? This could produce some fantastically unexpected results in the majority of scripts I have been reviewing with error handling in them.

What if I really need to terminate, do I have to do error detection flags like the following to overcome it?
  1. function Test-WriteError {  
  2.     [CmdletBinding()] param()  
  3.   
  4.     "Test-WriteError::ErrorActionPreference = $ErrorActionPreference"  
  5.   
  6.     try {  
  7.         Move-Item -Path 'C:\Does\Not\Exists.log' -Destination 'C:\No\Where' -ErrorAction Stop  
  8.     } catch {  
  9.         throw $_.Exception.Message  
  10.         $die = $true  
  11.     }  
  12.   
  13.     if (!$die) {  
  14.         "Test-WriteError::End"  
  15.     }  
  16. }  

Test-WriteError -ErrorAction SilentlyContinue
Test-WriteError::ErrorActionPreference = SilentlyContinue
Test-WriteError::End

Would you like to guess why that doesn't work as intended? Because throw is a terminating error but it continues with the rest of the script because of SilentlyContinue.

Just move that $die = $true statement to be above the throw statement in the catch block and all works as intended. This also means using a break statement after the throw won't work either because that code never executes and you can't break before you throw because then you never throw. Paradox 1. PowerShell 0.

But let's be honest, this just sucks. I want a terminating error to really be one no matter what $ErrorActionPreference is set to.

Write-Error Save Me!
This may not be the only way to get around this, but it works. It really sucks we can't reliably use the throw statement to produce a terminating error. Maybe they'll be inclined to fix this in the next version if we can spread the word of this non-trivial annoyance.

There are two ways Write-Error can be used to produce consistent behavior. I like this one as it is one line, but it is inconsistent with the intended usage of Write-Error as it is intended to indicate a non-terminating error.

You could opt to use Write-Error then the break statement after that to indicate the rest of the script will not execute but I'm not sure that is any clearer than this solution, but it is more consistent with the intended usage of Write-Error and the break statement.
  1. function Test-WriteError {  
  2.     [CmdletBinding()] param()  
  3.   
  4.     "Test-WriteError::ErrorActionPreference = $ErrorActionPreference"  
  5.   
  6.     try {  
  7.         Move-Item -Path 'C:\Does\Not\Exists.log' -Destination 'C:\No\Where' -ErrorAction Stop  
  8.     } catch {  
  9.         Write-Error $_.Exception.Message -ErrorAction Stop  
  10.     }  
  11.   
  12.     "Test-WriteError::End"  
  13. }  
This produces a consistent flow by causing a terminating error that really terminates out of the current pipeline if that is what you need or want to happen. The -ErrorAction parameter set to Stop on Write-Error produces a terminating error which stops the pipeline, even if someone called your cmdlet with the -ErrorAction parameter set to SilentlyContinue.

Like I said, it would be wonderful if the throw statement had this same behavior.

Don't ask me why, it's Friday at 4:30pm and it's time to get the weekend started.

Happy Weekend Scripting!

Comments

Popular posts from this blog

PowerShell SupportsShouldProcess Worst & Best Practices

This has been a very big discussion within the Scripting Games 2013 community and I want to add my two cents in an official blog post.

I've left several people comments on how they might be misunderstanding how SupportsShouldProcess works, but I also realize, everyone of these individuals has given me more insight into its use and perhaps, how it should best be utilized.

For those of you that don't know, SupportsShouldProcess is a parameter on the CmdletBinding attribute you can place on your cmdlets that automatically adds the -WhatIf and -Confirm parameters. These will naturally flow into other cmdlets you use that also SupportsShouldProcess, e.g. New-Item, Move-Item.

The major discussion has been around, should you just let the other cmdlets handle the $PSCmdlet.ShouldProcess feature, and if not how should you implement it. ShouldProcess has the following definitions.
OverloadDefinitions�����������������������������������������������������������������������������������������…

FIM 2010 R2 Self-Service Password Reset Auto-Registration

I have been working on our FIM 2010 R2 SP1 lab environment looking for ways to simplify some of the overly complicated scenarios we had to implement to workaround the limitations in FIM 2010. One of those workarounds was the auto-registration of SSPR for new employees. When we onboard a new employee, we want to create a simple SSPR for them to get their first-time password reset.

Prior to the R2 release we were using the http://fim2010client.codeplex.com/ client and PowerShell to complete the registration process on a daily schedule. I was working on converting this to use the R2 registration cmdlets and combined it with PowerShell 3.0 Workflows to get to a solution similar to this.
[CmdletBinding()]  param()  begin {      workflow Invoke-RegisterSSPR {          InlineScript {              Import-Module FIM          }  $workflow = InlineScript { Get-FIMResource -Filter '/WorkflowDefinition[DisplayName = "Password Reset AuthN Workflow for New Employees"]' }  $members  …