Powershell Error Handling

Based on original article that can be found here: http://powershell.com/cs/blogs/ebookv2/archive/2012/03/18/chapter-11-error-handling.aspx

When you design a PowerShell script, there may be situations where you cannot eliminate all possible runtime errors. If your script maps network drives, there could be a situation where no more drive letters are available, and when your script performs a remote WMI query, the remote machine may not be available.

In this chapter, you learn how to discover and handle runtime errors gracefully.

Topics Covered:

Suppressing Errors

Every cmdlet has built-in error handling which is controlled by the -ErrorAction parameter. The default ErrorAction is "Continue": the cmdlet outputs errors but continues to run.

This default is controlled by the variable $ErrorActionPreference. When you assign a different setting to this variable, it becomes the new default ErrorAction. The default ErrorAction applies to all cmdlets that do not specify an individual ErrorAction by using the parameter -ErrorAction.

To suppress error messages, set the ErrorAction to SilentlyContinue. For example, when you search the windows folder recursively for some files or folder, your code may eventually touch system folders where you have no sufficient access privileges. By default, PowerShell would then throw an exception but would continue to search through the subfolders. If you just want the files you can get your hands on and suppress ugly error messages, try this:

PS> Get-Childitem $env:windir -ErrorAction SilentlyContinue -recurse -filter *.log

Likewise, if you do not have full local administrator privileges, you cannot access processes you did not start yourself. Listing process files would produce a lot of error messages. Again, you can suppress these errors to get at least those files that you are able to access:

Get-Process -FileVersion -ErrorAction SilentlyContinue

Suppress errors with care because errors have a purpose, and suppressing errors will not solve the underlying problem. In many situations, it is invaluable to receive errors, get alarmed and act accordingly. So only suppress errors you know are benign.

NOTE: Sometimes, errors will not get suppressed despite using SilentlyContinue. If a cmdlet encounters a serious error (which is called "Terminating Error"), the error will still appear, and the cmdlet will stop and not continue regardless of your ErrorAction setting.

Whether or not an error is considered "serious" or "terminating" is solely at the cmdlet authors discretion. For example, Get-WMIObject will throw a (non-maskable) terminating error when you use -ComputerName to access a remote computer and receive an "Access Denied" error. If Get-WMIObject encounters an "RPC system not available" error because the machine you wanted to access is not online, that is considered not a terminating error, so this type of error would be successfully suppressed.

Handling Errors

To handle an error, your code needs to become aware that there was an error. It then can take steps to respond to that error. To handle errors, the most important step is setting the ErrorAction default to Stop:

$ErrorActionPreference = 'Stop'

As an alternative, you could add the parameter -ErrorAction Stop to individual cmdlet calls but chances are you would not want to do this for every single call except if you wanted to handle only selected cmdlets errors. Changing the default ErrorAction is much easier in most situations.

The ErrorAction setting not only affects cmdlets (which have a parameter -ErrorAction) but also native commands (which do not have such a parameter and thus can only be controlled via the default setting).

Once you changed the ErrorAction to Stop, your code needs to set up an error handler to become aware of errors. There is a local error handler (try/catch) and also a global error handler (trap). You can mix both if you want.

Try/Catch

To handle errors in selected areas of your code, use the try/catch statements. They always come as pair and need to follow each other immediately. The try-block marks the area of your code where you want to handle errors. The catch-block defines the code that is executed when an error in the try-block occurs.

Take a look at this simple example:

'localhost', '127.0.0.1', 'storage1', 'nonexistent', 'offline' |
  ForEach-Object {
    try {
      Get-WmiObject -class Win32_BIOS -computername $_ -ErrorAction Stop | 
        Select-Object __Server, Version
    }
    catch {
      Write-Warning "Error occured: $_"
    }
  }

It takes a list of computer names (or IP addresses) which could also come from a text file (use Get-Content to read a text file instead of listing hard-coded computer names). It then uses Foreach-Object to feed the computer names into Get-WMIObject which remotely tries to get BIOS information from these machines.

Get-WMIObject is encapsulated in a try-block and also uses the ErrorAction setting Stop, so any error this cmdlet throws will execute the catch-block. Inside the catch-block, in this example a warning is outputted. The reason for the error is available in $_ inside the catch-block.

Try and play with this example. When you remove the -ErrorAction parameter from Get-WMIObject, you will notice that errors will no longer be handled. Also note that whenever an error occurs in the try-block, PowerShell jumps to the corresponding catch-block and will not return and resume the try-block. This is why only Get-WMIObject is placed inside the try-block, not the Foreach-Object statement. So when an error does occur, the loop continues to run and continues to process the remaining computers in your list.

The error message created by the catch-block is not yet detailed enough:

WARNING: Error occured: The RPC server is unavailable. (Exception from HRESULT:0x800706BA)

You may want to report the name of the script where the error occured, and of course you'd want to output the computer name that failed. Here is a slight variant which accomplishes these tasks. Note also that in this example, the general ErrorActionPreference was set to Stop so it no longer is necessary to submit the -ErrorAction parameter to individual cmdlets:

'localhost', '127.0.0.1', 'storage1', 'nonexistent', 'offline' |
  ForEach-Object {
    try {
      $ErrorActionPreference = 'Stop'
      $currentcomputer = $_
      Get-WmiObject -class Win32_BIOS -computername $currentcomputer  | 
        Select-Object __Server, Version
    }
    catch {
      Write-Warning ('Failed to access "{0}" : {1} in "{2}"' -f $currentcomputer, `
      $_.Exception.Message, $_.InvocationInfo.ScriptName)
    }
  }

This time, the warning is a lot more explicit:

WARNING: Failed to access "nonexistent" : The RPC server is unavailable. 
(Exception from HRESULT: 0x800706BA) in "C:\Users\w7-pc9\AppData\Local\Temp\Untitled3.ps1"

Here, two procedures were needed: first of all, the current computer name processed by Foreach-Object needed to be stored in a new variable because the standard $_ variable is reused inside the catch-block and refers to the current error. So it can no longer be used to read the current computer name. That's why the example stored the content of $_ in $currentcomputer before an error could occur. This way, the script code became more legible as well.

Second, inside the catch-block, $_ resembles the current error. This variable contains a complex object which contains all details about the error. Information about the cause can be found in the property Exception whereas information about the place the error occured are found in InvocationInfo.

To examine the object stored in $_, you can save it in a global variable. This way, the object remains accessible (else it would be discarded once the catch-block is processed). So when an error was handled, you can examine your test variable using Get-Member. This is how you would adjust the catch-block:

    catch {
      $global:test = $_
      Write-Warning ('Failed to access "{0}" : {1} in "{2}"' -f $currentcomputer, `
      $_.Exception.Message, $_.InvocationInfo.ScriptName)
    }
  }

Then, once the script ran (and encountered an error), check the content of $test:

PS> Get-Member -InputObject $test


   TypeName: System.Management.Automation.ErrorRecord

Name                  MemberType     Definition
----                  ----------     ----------
Equals                Method         bool Equals(System.Object obj)
GetHashCode           Method         int GetHashCode()
GetObjectData         Method         System.Void GetObjectData(System.Runtime.Seriali...
GetType               Method         type GetType()
ToString              Method         string ToString()
CategoryInfo          Property       System.Management.Automation.ErrorCategoryInfo C...
ErrorDetails          Property       System.Management.Automation.ErrorDetails ErrorD...
Exception             Property       System.Exception Exception {get;}
FullyQualifiedErrorId Property       System.String FullyQualifiedErrorId {get;}
InvocationInfo        Property       System.Management.Automation.InvocationInfo Invo...
PipelineIterationInfo Property       System.Collections.ObjectModel.ReadOnlyCollectio...
TargetObject          Property       System.Object TargetObject {get;}
PSMessageDetails      ScriptProperty System.Object PSMessageDetails {get=& { Set-Stri...

As you see, the error information has a number of subproperties like the one used in the example. One of the more useful properties is InvocationInfo which you can examine like this:

PS> Get-Member -InputObject $test.InvocationInfo


   TypeName: System.Management.Automation.InvocationInfo

Name             MemberType Definition
----             ---------- ----------
Equals           Method     bool Equals(System.Object obj)
GetHashCode      Method     int GetHashCode()
GetType          Method     type GetType()
ToString         Method     string ToString()
BoundParameters  Property   System.Collections.Generic.Dictionary`2[[System.String, m...
CommandOrigin    Property   System.Management.Automation.CommandOrigin CommandOrigin ...
ExpectingInput   Property   System.Boolean ExpectingInput {get;}
HistoryId        Property   System.Int64 HistoryId {get;}
InvocationName   Property   System.String InvocationName {get;}
Line             Property   System.String Line {get;}
MyCommand        Property   System.Management.Automation.CommandInfo MyCommand {get;}
OffsetInLine     Property   System.Int32 OffsetInLine {get;}
PipelineLength   Property   System.Int32 PipelineLength {get;}
PipelinePosition Property   System.Int32 PipelinePosition {get;}
PositionMessage  Property   System.String PositionMessage {get;}
ScriptLineNumber Property   System.Int32 ScriptLineNumber {get;}
ScriptName       Property   System.String ScriptName {get;}
UnboundArguments Property   System.Collections.Generic.List`1[[System.Object, mscorli...

It tells you all details about the place the error occured.

Using Traps

If you do not want to focus your error handler on a specific part of your code, you can also use a global error handler which is called "Trap". Actually, a trap really is almost like a catch-block without a try-block. Check out this example:

trap {
      Write-Warning ('Failed to access "{0}" : {1} in "{2}"' -f $currentcomputer, `
      $_.Exception.Message, $_.InvocationInfo.ScriptName)
      continue
    }

'localhost', '127.0.0.1', 'storage1', 'nonexistent', 'offline' |
  ForEach-Object {
      $currentcomputer = $_
      Get-WmiObject -class Win32_BIOS -computername $currentcomputer -ErrorAction Stop  | 
        Select-Object __Server, Version
  }

This time, the script uses a trap at its top which looks almost like the catch-block used before. It does contain one more statement to make it act like a catch-block: Continue. Without using Continue, the trap would handle the error but then forward it on to other handlers including PowerShell. So without Continue, you would get your own error message and then also the official PowerShell error message.

When you run this script, you will notice differences, though. When the first error occurs, the trap handles the error just fine, but then the script stops. It does not execute the remaining computers in your list. Why?

Whenever an error occurs and your handler gets executed, it continues execution with the next statement following the erroneous statement - in the scope of the handler. So when you look at the example code, you'll notice that the error occurred inside the Foreach-Object loop. Whenever your code uses braces, the code inside the braces resembles a new "territory" or "scope". So the trap did process the first error correctly and then continued with the next statement in its own scope. Since there was no code following your loop, nothing else was executed.

This example illustrates that it always is a good idea to plan what you want your error handler to do. You can choose between try/catch and trap, and also you can change the position of your trap.

If you placed your trap inside the "territory" or "scope" where the error occurs, you could make sure all computers in your list are processed:

'localhost', '127.0.0.1', 'storage1', 'nonexistent', 'offline' |
  ForEach-Object {
      trap {
        Write-Warning ('Failed to access "{0}" : {1} in "{2}"' -f $currentcomputer, `
        $_.Exception.Message, $_.InvocationInfo.ScriptName)
        continue
      }
      
      $currentcomputer = $_
      Get-WmiObject -class Win32_BIOS -computername $currentcomputer -ErrorAction Stop  | 
        Select-Object __Server, Version
  }

Handling Native Commands

Most errors in your PowerShell code can be handled in the way described above. The only command type that does not fit into this error handling scheme are native commands. Since these commands were not developed specifically for PowerShell, and since they do not necessarily use the .NET framework, they cannot directly participate in PowerShells error handling.

Console-based applications return their error messages through another mechanism: they emit error messages using the console ErrOut channel. PowerShell can monitor this channel and treat outputs that come from this channel as regular exceptions. To make this work, you need to do two things: first of all, you need to set $ErrorActionPreference to Stop, and second, you need to redirect the ErrOut channel to the StdOut channel because only this channel is processed by PowerShell. Here is an example:

When you run the following native command, you will receive an error, but the error is not red nor does it look like the usual PowerShell error messages because it comes as plain text directly from the application you ran:

PS> net user willibald
The user name could not be found.

More help is available by typing NET HELPMSG 2221.

When you redirect the error channel to the output channel, the error suddenly becomes red and is turned into a "real" PowerShell error:

PS> net user willibald 2>&1
net.exe : The user name could not be found.
At line:1 char:4
+ net <<<<  user willibald 2>&1
    + CategoryInfo          : NotSpecified: (The user name could not be found.:String)
   [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError

More help is available by typing NET HELPMSG 2221.

You can still not handle the error. When you place the code in a try/catch-block, the catch-block never executes:

try {
  net user willibald 2>&1
  }
  
catch {
  Write-Warning "Oops: $_"
}

As you know from cmdlets, to handle errors you need to set the ErrorAction to Stop. With cmdlets, this was easy because each cmdlet has a -ErrorAction preference. Native commands do not have such a parameter. This is why you need to use $ErrorActionPreference to set the ErrorAction to Stop:

try {
  $ErrorActionPreference = 'Stop'
  net user willibald 2>&1
  }
  
catch {
  Write-Warning "Oops: $_"
}

If you do not like the default colors PowerShell uses for error messages, simply change them:

$Host.PrivateData.ErrorForegroundColor = "Red"
$Host.PrivateData.ErrorBackgroundColor = "White"

You can also find additional properties in the same location which enable you to change the colors of warning and debugging messages (like WarningForegroundColor and WarningBackgroundColor).

Understanding Exceptions

Exceptions work like bubbles in a fish tank. Whenever a fish gets sick, it burps, and the bubble bubbles up to the surface. If it reaches the surface, PowerShell notices the bubble and throws the exception: it outputs a red error message.

In this chapter, you learned how you can catch the bubble before it reaches the surface, so PowerShell would never notice the bubble, and you got the chance to replace the default error message with your own or take appropriate action to handle the error.

The level the fish swims in the fish tank resembles your code hierarchy. Each pair of braces resembles own "territory" or "scope", and when a scope emits an exception (a "bubble"), all upstream scopes have a chance to catch and handle the exception or even replace it with another exception. This way you can create complex escalation scenarios.

Handling Particular Exceptions

The code set by Trap is by default executed for any (visible) exception. If you'd prefer to use one or several groups of different error handlers, write several Trap (or Catch) statements and specify for each the type of exception it should handle:

function Test
{
  trap [System.DivideByZeroException] { "Divided by null!"; continue }
  trap [System.Management.Automation.ParameterBindingException] {
        "Incorrect parameter!";
        continue
        }
  1/$null
  Dir -MacGuffin
}
Test
Divided by null!
Incorrect parameter!

Throwing Your Own Exceptions

If you develop functions or scripts and handle errors, you are free to output error information any way you want. You could output it as plain text, use a warning or write error information to a log file. With any of these, you take away the opportunity for the caller to respond to errors - because the caller has no longer a way of detecting the error. That's why you can also throw your own exceptions. They can be caught by the caller using a trap or a try/catch-block.

function TextOutput([string]$text)
{
  if ($text -eq "")
  {
    Throw "You must enter some text."
  }
  else
  {
    "OUTPUT: $text"
  }
}

# An error message will be thrown if no text is entered:
TextOutput
You have to enter some text.
At line:5 char:10
+     Throw  <<<< "You have to enter some text."

# No error will be output in text output:
TextOutput Hello
OUTPUT: Hello

The caller can now handle the error your function emitted and choose by himself how he would like to respond to it:

PS> try { TextOutput } catch { "Oh, an error: $_" }
Oh, an error: You must enter some text.

Stepping And Tracing

Commercial PowerShell development environments like PowerShellPlus from Idera make it easy for you to set breakpoints and step through code to see what it actually does. In larger scripts, this is an important diagnostic feature to debug code.

However, PowerShell has also built-in methods to step code or trace execution. To enable tracing, use this:

PS> Set-PSDebug -trace 1
PS> dir
DEBUG:    1+  <<<< dir
DEBUG:    1+ $_.PSParentPath.Replace <<<< ("Microsoft.PowerShell.Core\FileSystem::", "")
DEBUG:    2+                                     [String]::Format <<<< ("{0,10}  {1,8}",
 $_.LastWriteTime.ToString("d"), $_.LastWriteTime.ToString("t"))


    Directory: C:\Users\w7-pc9


Mode                LastWriteTime     Length Name
----                -------------     ------ ----
d----        30.11.2009     12:54            Application data
DEBUG:    2+                                     [String]::Format <<<< ("{0,10}  {1,8}",
 $_.LastWriteTime.ToString("d"), $_.LastWriteTime.ToString("t"))
d-r--        04.08.2010     06:36            Contacts
(...)

Simple tracing will show you only PowerShell statements executed in the current context. If you invoke a function or a script, only the invocation will be shown but not the code of the function or script. If you would like to see the code, turn on detailed traced by using the -trace 2 parameter.

Set-PSDebug -trace 2

If you would like to turn off tracing again, select 0:

Set-PSDebug -trace 0

To step code, use this statement:

Set-PSDebug -step

Now, when you execute PowerShell code, it will ask you for each statement whether you want to continue, suspend or abort.

If you choose Suspend by pressing "H", you will end up in a nested prompt, which you will recognize by the "<<" sign at the prompt. The code will then be interrupted so you could analyze the system in the console or check variable contents. As soon as you enter Exit, execution of the code will continue. Just select the "A" operation for "Yes to All" in order to turn off the stepping mode.

Tip: You can create simple breakpoints by using nested prompts: call $host.EnterNestedPrompt() inside a script or a function.

Set-PSDebug has another important parameter called -strict. It ensures that unknown variables will throw an error. Without the Strict option, PowerShell will simply set a null value for unknown variables. On machines where you develop PowerShell code, you should enable strict mode like this:

Set-StrictMode -Version Latest

This will throw exceptions for unknown variables (possible typos), nonexistent object properties and wrong cmdlet call syntax.

Summary

To handle errors in your code, make sure you set the ErrorAction to Stop. Only then will cmdlets and native commands place errors in your control.

To detect and respond to errors, use either a local try/catch-block (to catch errors in specific regions of your code) or trap (to catch all errors in the current scope). With trap, make sure to also call Continue at the end of your error handler to tell PowerShell that you handled the error. Else, it would still bubble up to PowerShell and cause the default error messages.

To catch errors from console-based native commands, redirect their ErrOut channel to StdOut. PowerShell then automatically converts the custom error emitted by the command into a PowerShell exception.