Effective PowerShell Item 16: Dealing with Errors

There are several facets to the subject of errors in PowerShell that you should understand to get the most out of PowerShell.  Some of these facets are error handling, error related global variables and error related preference variables.  But the most fundamental facet is the distinction between “terminating” and “non-terminating” errors.

Terminating Errors

Terminating errors will be immediately familiar to software developers who deal with exceptions.  If an exception is not handled it will cause the program to crash.  Similarly if a terminating error is not handled it will cause the current operation (cmdlet or script) to abort with an error.  Terminating errors and are generated by:

  • Cmdlet calling the ThrowTerminatingError API.
  • Exceptions escaping unhandled from a cmdlet
  • Script using the “throw” keyword to issue a terminating error
  • Script syntax errors

The gist of a terminating error is that the code throwing the terminating error is indicating that it cannot reasonably continue and is aborting the requested operation.  As we will see later, you as the client of that code, have the ability to declare that you can handle the error and continue executing subsequent commands.  Terminating errors that are not handled propagate up through the calling code, prematurely terminating each calling function or script until either the error is handled or the original invoking operation is terminated.

Here is an example of how a terminating error alters control flow:

PS> "Before"; throw "Oops!"; "After"
Before
Oops!
At line:1 char:16
+ "Before"; throw <<<<  "Oops!"; "After"
    + CategoryInfo          : OperationStopped: (Oops!:String) [], RuntimeException
    + FullyQualifiedErrorId : Oops!

Note that “After” is not output to the console because “throw” issues a terminating error.

Non-terminating Errors

Have you ever experienced the following in older versions of Windows Explorer?  You open a directory with a large number of files, say your temp dir, and you want to empty it.  You select the entire contents of the directory, press Delete and wait.  Unfortunately some processes invariably have files open in the temp directory.  So after deleting a few files, you get an error from Windows Explorer indicating that it can’t delete some file.  You press OK and at this point Windows Explorer aborts the operation.  It treats the error effectively as a terminating error.  This can be very frustrating.  You select everything again, press Delete, Explorer deletes a few more files then errors and aborts again.  You rinse and repeat these steps until finally all the files that can be deleted are deleted.  This behavior is very annoying  and wastes your time.  In an automation scenario, premature aborts like this are often unacceptable.

Having a special category of error that does not terminate the current operation is very useful in scenarios like the one outlined above. In PowerShell, that category is the non-terminating error.  Even though a non-terminating error does not terminate the current operation, the error is still logged to the $Error collection (discussed later) as well as displayed on the host’s console as is the case with terminating errors.  Non-terminating errors are generated by:

  • Cmdlet calling the WriteError API.
  • Script using the Write-Error cmdlet to log a non-terminating error
  • Exceptions thrown from calls to a member of a .NET object or type.

Here is an example of how a non-terminating error does not alter control flow:

PS> "Before"; Write-Error "Oops!"; "After"
Before
"Before"; Write-Error "Oops!"; "After" : Oops!
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException

After

Note the Write-Error command issues a non-terminating error that gets displayed on the host’s console then the script continues execution.

Error Variables

There are several global variables and global preference variables related to errors.  Here is a brief primer on them:

  • $? – contains the execution status of the last operation.  True indicates the operation succeeded without any errors.  False indicates either complete failure or partial success.  Note: for Windows executables the exit code is examined.  An exit code of 0 will be interpreted as success and non-zero as failure.  Some Windows console apps don’t honor this convention so it is usually better to inspect $LASTEXITCODE such that you can determine for yourself success or failure based your interpretation of the exit code.
  • $LASTEXITCODE – exit code of the last Windows executable invoked from this session.
  • $Error – collection (ArrayList to be specific) of errors that have occurred in the current session.  Errors are always inserted at the beginning of the collection.  As a result, the most recent error is always located at index 0.
  • $MaximumErrorCount – determines the size of the $Error collection.  Defaults to 256 which is the minimum value allowed.  Max value is 32768.
  • $ErrorActionPreference – influences the dispatching of non-terminating errors.  The default is ‘Continue’ which adds an entry to the $Error collection and displays the error on the host’s console.
  • $ErrorView – specifies one of two views for error records when they’re displayed on the host.  The default is ‘NormalView’ which displays several lines of information.  For production environments, you can set this to ‘CategoryView’ to get a succinct one line error message.  Remember that all the details are still available in the $Error collection.

The $Error global variable can be used to inspect the details of up to the last $MaximumErrorCount number of errors that have occurred during the session e.g.:

PS> $error[0] | fl * -force

PSMessageDetails      :
Exception             : System.IO.IOException: The process cannot access the file ‘\Temp\FX
                        SAPIDebugLogFile.txt’ because it is being used by another process.
                           at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
                           at System.IO.FileInfo.Delete()
                           at Microsoft.PowerShell.Commands.FileSystemProvider.RemoveFileSystemItem(FileSystemInfo file
                        SystemInfo, Boolean force)
TargetObject          : \Temp\FXSAPIDebugLogFile.txt
CategoryInfo          : WriteError: (\Temp\FXSAPIDebugLogFile.txt:FileInfo) [Remove-Item], IOException
FullyQualifiedErrorId : RemoveFileSystemItemIOError,Microsoft.PowerShell.Commands.RemoveItemCommand
ErrorDetails          : Cannot remove item \Temp\FXSAPIDebugLogFile.txt: The process cannot
                         access the file ‘\Temp\FXSAPIDebugLogFile.txt’ because it is being
                         used by another process.
InvocationInfo        : System.Management.Automation.InvocationInfo
PipelineIterationInfo : {0, 1}

As the output above shows, errors in PowerShell are not just strings but rich objects.  The object may be a .NET exception with an embedded error record or just an error record,  The error record contains lots of useful information about the error and the context in which it occurred.

The default output formatting of errors can be a bit hard to digest.  The PowerShell Community Extensions come with a handy Resolve-Error function that digs through the error information and surfaces the important stuff e.g.:

PS> Resolve-Error # displays $error[0] by default

PS> Resolve-Error $error[1]

The $? global variable is handy for determining if the last operation encountered any errors e.g.:

PS> Remove-Item $env:temp\*.txt -Recurse -Verbose
VERBOSE: Performing operation "Remove File" on Target "…\Temp\foo.txt".
VERBOSE: Performing operation "Remove File" on Target "…\Temp\FXSAPIDebugLogFile.txt".
WriteError: (…\Temp\DebugLogFile.txt:FileInfo) [Remove-Item], IOException
PS> $?
False

In this case, the Remove-Item cmdlet only partially succeeded.  It deleted two files but then encountered a non-terminating error. This failure to achieve complete success i.e. no errors, is indicated by $? returning False.

Working with Non-Terminating Errors

Sometimes you want to completely ignore non-terminating errors.  Who wants all that red text spilled all over their console especially when you don’t care about the errors you know you’re going to get.  You can suppress the display of non-terminating errors either locally or globally.  To do this locally, just set the cmdlet’s ErrorAction parameter to SilentlyContinue e.g.

Remove-Item $env:temp\*.txt -Recurse -Verbose -ErrorAction SilentlyContinue

For interactive scenarios it is handy to use 0 instead of SilentlyContinue.  This works because SilentlyContinue is part of a enum and its integer value is 0.   So to save your wrists you can rewrite the above as:

ri $env:temp\*.txt -r -v –ea 0

Note that for a script I would use the first approach for readability.

To accomplish the above globally, set the $ErrorActionPreference global preference variable to SilentlyContinue (or 0).  This will cause all non-terminating errors in the session to not be displayed on the host’s console.  However they will still be logged to the $Error collection. 

Setting the $ErrorActionPreference to Stop can be useful in the following scenario.  If you misspell a command, PowerShell will generate a non-terminating error as shown below:

PS> Copy-Itme .\_lesshst .\_lesshst.bak; $?; "After"
The term ‘Copy-Itme’ is not recognized as the name of a cmdlet, function, scrip
t file, or operable program. Check the spelling of the name, or if a path was i
ncluded, verify that the path is correct and try again.
At line:1 char:10
+ Copy-Itme <<<<  .\_lesshst .\_lesshst.bak; $?; "After"
    + CategoryInfo          : ObjectNotFound: (Copy-Itme:String) [], CommandNo
   tFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

False
After

In this case, the misspelled Copy-Itme command failed ($? returned False) but since the error was non-terminating, the script continues execution as shown by the output “After”. 

If you are hard-core about correctness you can get PowerShell to convert non-terminating errors into terminating errors by setting $ErrorActionPreference to Stop which has global impact.  You can also do this one a cmdlet by cmdlet basis by setting the cmdlet’s –ErrorAction parameter to Stop.

The last issue to be aware of regarding non-terminating errors is that a Windows executable that returns a non-zero exit code does not generate any sort of error.  The only action PowerShell takes is to set $? to False if the exit code is non-zero.  There is no error record created and stuffed into $Error.  In many cases, the failure of an external executable means your script cannot continue.  In this case, it is desirable to convert a failure exit code into a terminating error.  This can be done easily using the function below:

function CheckLastExitCode {
    param ([int[]]$SuccessCodes = @(0), [scriptblock]$CleanupScript=$null)

    if ($SuccessCodes -notcontains $LastExitCode) {
        if ($CleanupScript) {
            "Executing cleanup script: $CleanupScript"
            &$CleanupScript
        }
        $msg = @"
EXE RETURNED EXIT CODE $LastExitCode
CALLSTACK:$(Get-PSCallStack | Out-String)
"@
        throw $msg
    }
}

Note that Get-PSCallStack is specific to PowerShell v2.0.  Invoke CheckLastExitCode right after invoking an executable, well at least for those cases where you care if an executable returns an error.  This function provides a couple of handy features.  First, you can specify an array of acceptable success codes which is useful for exes that return 0 for failure and 1 for success and is also useful for exes that return multiple success codes.  Second, you specify a cleanup scriptblock that will get executed on failure.

Handling Terminating Errors

Handling terminating errors in PowerShell comes in two flavors.  Using the trap keyword which is supported in both version 1 and 2 of PowerShell.  Using try { } catch { } finally { } which is new to version 2.

Trap Statement

Trap is a mechanism available in other shell languages like Korn shell.  It effectively declares that either any error type or a specific error type is handled by the scriptblock following the trap keyword.  Trap has the interesting property that where ever it is declared in a scope, it is valid for that entire scope e.g.:

Given the following script (trap.ps1):

"Before"
throw "Oops!"
"After"
trap { "Error trapped: $_" }

Invoking it results in the following output:

PS> .\trap.ps1
Before
Error trapped: Oops!
Oops!
At C:\Users\Keith\trap.ps1:2 char:6
+ throw <<<<  "Oops!"
    + CategoryInfo          : OperationStopped: (Oops!:String) [], RuntimeException
    + FullyQualifiedErrorId : Oops!

After

Note that it doesn’t matter that the trap statement is after the line that throws the error.  Also note that since the default value for $ErrorActionPreference is ‘Continue’, the error is displayed, logged to $Error but execution resumes at the next statement.  Note: within the context of a trap statement, $_ represents the error that was caught.

Another thing to consider is whether to use Write-Host or Write-Output to display text in the trap statement.  The example above implicitly uses Write-Output.  This has the benefit that the text can be redirected to a log file.  The downside is that if the exception is handled and execution continues, that text will become part of the output for that scope which, in the case of functions and scripts, may not be desirable.

If you want to execute cleanup code on failure but still terminate execution, we can change the trap statement to use the break keyword.  Consider the following script:

function Cleanup() {"cleaning up"}
trap { "Error trapped: $_"; continue }
"Outer Before"
& {
    trap { Cleanup; break }
    "Inner Before"
    throw "Oops!"
    "Inner After"
    Cleanup
}   
"Outer After"

Note that the inner trap calls the Cleanup function but then propagates the error.  As a result, the “Inner After” statement never executes because control flow is transferred outside the scope of the trap statement.  The outer trap then catches the error, displays it and continues execution.  As a result, the “Outer After” statement is executed.

The interaction between the control flow altering keywords valid in a trap statement (break, continue and return), the $ErrorActionPreference variable if no control flow altering keyword is used and the final behavior is somewhat complex as is demonstrated by the table below:

Trap Behavior:

Keyword Used Rely on $ErrorActionPreference Displays Error Propagates Error
break Stop True True
continue SilentlyContinue False False
return Continue True False
return<object>* N/A True False
N/A Inquire Depends upon response Depends upon response

* <object> is appended to the end of the trap scope’s output.

All of the examples of trap shown above trap all errors.  You may want to trap only specific errors.  You can do this by specifying the type name of an exception to trap as shown below:

trap [System.DivideByZeroException] { "Please don’t divide by 0!"}
$divisor = 0
1/$divisor

If you want to execute different code for different errors, you can define multiple trap statements in your script:

trap [System.DivideByZeroException] { "Please don’t divide by 0!"}

trap [System.Management.Automation.CommandNotFoundException] { "Did you fat finger the command name?" }
trap { "Anything not caught by the first two traps gets here" }

If you define multiple trap statements for the same error type the first one wins and the others within the same scope are ignored.

Try / Catch / Finally

Version 2 of Windows PowerShell introduces try/catch/finally statements – a new error handling mechanism that most developers will be immediately familiar with.  There are two main differences between trap and try/catch/finally.  First, a trap anywhere in a lexical scope covers the entire lexical scope.  With a try statement, only the script within the try statement is checked for errors.  The second difference is that trap doesn’t support finally behavior i.e., always execute the finally statement whether the code in the try statement throws a terminating error or not.  In fact, any associated catch statements could also throw a terminating error and the finally statement would still execute. 

You can fake finally behavior with trap by calling the same “finally” code from the end of the lexical scope *and* from the trap statement.  Consider the Cleanup function from the earlier example.  We want to always execute Cleanup whether the script errors or not.  The example shown in the previous section using the Cleanup function works OK unless the Cleanup function throws a terminating error. Then you run into the issue where Cleanup gets called again due to the trap statement.   This sort of cleanup is much easier to represent in your script using try/finally e.g.:

function Cleanup($err) {"cleaning up"}
trap { "Error trapped: $_"; continue }

"Outer Before"
try {
    "Inner Before"
    throw "Oops!"
    "Inner After"
}   
finally {
    Cleanup
}
"Outer After"

This example results in Cleanup always getting called whether or not the script in the try statement generates a terminating error.  It also shows that you can mix and match trap statements with try/catch/finally.

One last example shows how you can use catch to handle different error types uniquely:

function Cleanup($err) {"cleaning up"}
trap { "Error trapped: $_"; continue }

"Outer Before"
try {
    "Inner Before"
    throw "Oops!"
    "Inner After"
}   
catch [System.DivideByZeroException] {
    "Please don’t divide by 0!"
}
catch [System.Management.Automation.CommandNotFoundException] {
    "Did you fat finger the command name?"
}
catch {
    "Anything not caught by the first two catch statements gets here"
}
finally {
    Cleanup
}
"Outer After"

The use of the finally statement is optional as is the catch statement.  The valid combinations are try/catch, try/finally and try/catch/finally.

In summary, PowerShell’s error handling capabilities are quite powerful especially the ability to distinguish between non-terminating and terminating errors.  With the addition of the new try/catch/finally support in version 2.0 the important scenario of resource cleanup is easy to handle.

Advertisements
This entry was posted in Effective PowerShell. Bookmark the permalink.

6 Responses to Effective PowerShell Item 16: Dealing with Errors

  1. Jack says:

    How can I tell powershell.exe to return nonzero exit code when there’s a syntax error in the script?

  2. Pingback: Powershell error handling: try/catch but can’t continue after error raise | Jacques DALBERA's IT world

  3. vitaly.krugl.junkmail@gmail.com says:

    When I set

    $ErrorActionPreference = “Stop”

    In a powershell script, it turns non-terminating errors into terminating errors. This is exactly what I need during installation of tools in the windows build to make sure that errors aren’t ignored. However, what I found with `$ErrorActionPreference = “Stop”` is that when an error occurs, powershell appears to suppress the actual error message from the failed executable, so I can’t tell what the executable didn’t like. For example, when I mistakenly called `pip -i …` instead of `pip install -i …`, `$ErrorActionPreference = “Stop”` suppressed the error message from pip that “there is not such option -i”, making it difficult to debug.

    Any idea how to prevent `$ErrorActionPreference = “Stop”` from suppressing the error messages from failed executables? Or get the desired effect in some other way?

    • rkeithhill says:

      I have simple C# program I keep around called stderr.exe that just echos whatever args I send it to stderr. When I set `$ErrorActionPreference = ‘Stop’`, I’m seeing stderr output from stderr.exe. I’m on PSV5 on Windows 10. BTW are you testing this from the console or ISE? ISE behaves differently WRT executables stderr output. ISE will convert exe stderr output to error records whereas the console will not. So in ISE you will see red text for the PowerShell error record with the original stderr output in it. In the console you will only see the original exe stderr output (and not in red).

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s