Chapter 15. Tracing and Error Management

Introduction

What if it doesn't all go according to plan? This is the core question behind error management in any system and plays a large part in writing PowerShell scripts as well.

While it is a core concern in many systems, PowerShell's support for error management provides several unique features designed to make your job easier: the primary benefit being a distinction between terminating and nonterminating errors.

When running a complex script or scenario, the last thing you want is for your world to come crashing down because a script can't open one of the 1,000 files it is operating on. Although the system should make you aware of the failure, the script should still continue to the next file. That is an example of a nonterminating error. But what if the script runs out of disk space while running a backup? That should absolutely be an error that causes the script to exit—also known as a terminating error.

Given this helpful distinction, PowerShell provides several features that let you manage errors generated by scripts and programs, and also allows you to generate them yourself.

Determine the Status of the Last Command

Problem

You want to get status information about the last command you executed, such as whether it succeeded.

Solution

Use one of the two variables PowerShell provides to determine the status of the last command you executed: the $lastExitCode variable and the $? variable.

1 comment

  1. David "Makovec" Moravec Posted 11 days and 12 hours ago

    $? - change font of '(pronounced "dollar hook")'

Add a comment

$lastExitCode

A number that represents the exit code/error level of the last script or application that exited

$? (Pronounced "Dollar Hook")

A Boolean value that represents the success or failure of the last command

Discussion

The $lastExitCode PowerShell variable is similar to the %errorlevel% variable in DOS. It holds the exit code of the last application to exit. This lets you continue to interact with traditional executables (such as ping, findstr, and choice) that use exit codes as a primary communication mechanism. PowerShell also extends the meaning of this variable to include the exit codes of scripts, which can set their status using the exit statement. Example 15.1, “Interacting with the $lastExitCode and $? variables” demonstrates this interaction.

Example 15.1. Interacting with the $lastExitCode and $? variables

PS > ping localhost

Pinging MyComputer [127.0.0.1] with 32 bytes of data:

Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128

Ping statistics for 127.0.0.1:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milliseconds:
    Minimum = 0ms, Maximum = 0ms, Average = 0ms
PS > $?
True
PS > $lastExitCode

0
PS > ping missing-host
Ping request could not find host missing-host. Please check the name and try again.
PS > $?
False
PS > $lastExitCode
1

The $? variable describes the exit status of the last application in a more general manner. PowerShell sets this variable to False on error conditions such as when:

For commands that do not indicate an error condition, PowerShell sets the $? variable to True.

View the Errors Generated by a Command

Problem

You want to view the errors generated in the current session.

Solution

To access the list of errors generated so far, use the $error variable, as shown by Example 15.2, “Viewing errors contained in the $error variable”.

Example 15.2. Viewing errors contained in the $error variable

PS > 1/0
Attempted to divide by zero.
At line:1 char:3
+ 1/ <<<< 0
    + CategoryInfo          : NotSpecified: (:) [], ParentContainsErrorRe
   cordException
    + FullyQualifiedErrorId : RuntimeException

PS > $error[0] | Format-List -Force


ErrorRecord                 : Attempted to divide by zero.
StackTrace                  :    at System.Management.Automation.Expressio
                              (...)
WasThrownFromThrowStatement : False
Message                     : Attempted to divide by zero.
Data                        : {}
InnerException              : System.DivideByZeroException: Attempted to d
                              ivide by zero.
                                 at System.Management.Automation.ParserOps
                              .PolyDiv(ExecutionContext context, Token opT
                              oken, Object lval, Object rval)
TargetSite                  : System.Collections.ObjectModel.Collection`1[
                              System.Management.Automation.PSObject] Invok
                              e(System.Collections.IEnumerable)
HelpLink                    :
Source                      : System.Management.Automation

Discussion

The PowerShell $error variable always holds the list of errors generated so far in the current shell session. This list includes both terminating and nonterminating errors.

PowerShell displays fairly detailed information when it encounters an error:

PS > Stop-Process -name IDoNotExist
Stop-Process : Cannot find a process with the name "IDoNotExist". Verify t
he process name and call the cmdlet again.
At line:1 char:13
+ Stop-Process <<<<  -name IDoNotExist
    + CategoryInfo          : ObjectNotFound: (IDoNotExist:String) [Stop-
   Process], ProcessCommandException
    + FullyQualifiedErrorId : NoProcessFoundForGivenName,Microsoft.PowerS
   hell.Commands.StopProcessCommand

One unique feature about these errors is that they benefit a diverse and international community of PowerShell users. Notice the FullyQualifiedErrorId line: an error identifier that remains the same no matter which language the error occurs in. When a user pastes this error message on an Internet forum, newsgroup or blog, this fully qualified error ID never changes. English users can then benefit from errors posted by non-English PowerShell users, and vice versa.

If you want to view an error in a table or list (through the Format-Table or Format-List cmdlets), you must also specify the -Force option to override this customized view.

If you want to display errors in a more compact manner, PowerShell supports an additional view called CategoryView that you set through the $errorView preference variable:

PS > Get-ChildItem IDoNotExist
Get-ChildItem : Cannot find path 'C:\IDoNotExist' because it does not exist.
At line:1 char:14
+ Get-ChildItem <<<<  IDoNotExist
    + CategoryInfo          : ObjectNotFound: (C:\IDoNotExist:String) [Ge
   t-ChildItem], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.
   GetChildItemCommand

PS > $errorView = "CategoryView"
PS > Get-ChildItem IDoNotExist
ObjectNotFound: (C:\IDoNotExist:String) [Get-ChildItem], ItemNotFoundExcep
tion

1 comment

  1. AndrewTearle Posted 16 days and 8 hours ago

    $errorView = "CategoryView" Excellent ! and useful

Add a comment

To clear the list of errors, call the Clear() method on the $error list:

PS > $error.Count
2
PS > $error.Clear()
PS > $error.Count
0

1 comment

  1. AndrewTearle Posted 16 days and 8 hours ago

    $error.Clear() Excellent ! and useful

Add a comment

For more information about PowerShell's preference variables, type Get-Help about_automatic_variables. If you want to determine only the success or failure of the last command, see the section called “Determine the Status of the Last Command”.

Manage the Error Output of Commands

Problem

You want to display detailed information about errors that come from commands.

Solution

To list all errors (up to $MaximumErrorCount) that have occurred in this session, access the $error array:

$error

To list the last error that occurred in this session, access the first element in the $error array:

$error[0]

To list detailed information about an error, pipe the error into the Format-List cmdlet with the -Force parameter:

$currentError = $error[0]
$currentError | Format-List -Force

To list detailed information about the command that caused an error, access its InvocationInfo property:

$currentError = $error[0]
$currentError.InvocationInfo

To display errors in a more succinct category-based view, change the $errorView variable to "CategoryView":

$errorView = "CategoryView"

To clear the list of errors collected by PowerShell so far, call the Clear() method on the $error variable:

$error.Clear()

Discussion

Errors are a simple fact of life in the administrative world. Not all errors mean disaster, though. Because of this, PowerShell separates errors into two categories: nonterminating and terminating.

Nonterminating errors are the most common type of error. They indicate that the cmdlet, script, function, or pipeline encountered an error that it was able to recover from or was able to continue past. An example of a nonterminating error comes from the Copy-Item cmdlet. If it fails to copy a file from one location to another, it can still proceed with the rest of the files specified.

A terminating error, on the other hand, indicates a deeper, more fundamental error in the operation. An example of this can again come from the Copy-Item cmdlet when you specify invalid command-line parameters.

Digging into an error (and its nested errors) can be cumbersome, so for a script that automates this task, see the section called “Program: Resolve an Error”.

Program: Resolve an Error

Analyzing an error frequently requires several different investigative steps: displaying the error, exploring its context, and analyzing its inner exceptions.

Example 15.3, “Resolve-Error.ps1” automates these mundane tasks for you.

Example 15.3. Resolve-Error.ps1

#############################################################################

<#

.SYNOPSIS

Displays detailed information about an error and its context

#>

param($errorRecord = ($error[0]))

""
"If this is an error in a script you wrote, use the Set-PsBreakpoint cmdlet"
"to diagnose it."
""

'Error details ($error[0] | Format-List * -Force)'
"-"*80
$errorRecord | Format-List * -Force

'Information about the command that caused this error ' +
    '($error[0].InvocationInfo | Format-List *)'
"-"*80
$errorRecord.InvocationInfo | Format-List *

'Information about the error''s target ' +
    '($error[0].TargetObject | Format-List *)'
"-"*80
$errorRecord.TargetObject | Format-List *

'Exception details ($error[0].Exception | Format-List * -Force)'
"-"*80

$exception = $errorRecord.Exception

for ($i = 0; $exception; $i++, ($exception = $exception.InnerException))
{
   "$i" * 80
   $exception | Format-List * -Force
}
      

For more information about running scripts, see the section called “Run Programs, Scripts, and Existing Tools”.

Configure Debug, Verbose, and Progress Output

Problem

You want to manage the detailed debug, verbose, and progress output generated by cmdlets and scripts.

Solution

To enable debug output for scripts and cmdlets that generate it:

$debugPreference = "Continue"
Start-DebugCommand

To enable verbose mode for a cmdlet that checks for the -Verbose parameter:

Copy-Item c:\temp\*.txt c:\temp\backup\ -Verbose

To disable progress output from a script or cmdlet that generates it:

$progressPreference = "SilentlyContinue"
Get-Progress.ps1

Discussion

In addition to error output (as described in the section called “Manage the Error Output of Commands”), many scripts and cmdlets generate several other types of output. This includes:

1 comment

  1. AndrewTearle Posted 16 days and 8 hours ago

    output (as described < no closing bracket

Add a comment

Debug output

Helps you diagnose problems that may arise and can provide a view into the inner workings of a command. You can use the Write-Debug cmdlet to produce this type of output in a script or the WriteDebug() method to produce this type of output in a cmdlet. PowerShell displays this output in yellow, unless you customize it through the $host.PrivateData.Debug* color configuration variables.

Verbose output

Helps you monitor the actions of commands at a finer level than the default. You can use the Write-Verbose cmdlet to produce this type of output in a script or the WriteVerbose() method to produce this type of output in a cmdlet. PowerShell displays this output in yellow, unless you customize it through the $host.PrivateData.Verbose* color configuration variables.

Progress output

Helps you monitor the status of long-running commands. You can use the Write-Progress cmdlet to produce this type of output in a script or the WriteProgress() method to produce this type of output in a cmdlet. PowerShell displays this output in yellow, unless you customize it through the $host.PrivateData.Progress* color configuration variables.

Some cmdlets generate verbose and debug output only if you specify the -Verbose and -Debug parameters, respectively.

To configure the debug, verbose, and progress output of a script or cmdlet, modify the $debugPreference, $verbosePreference, and $progressPreference shell variables. These variables can accept the following values:

SilentlyContinue

Do not display this output.

Stop

Treat this output as an error.

Continue

Display this output.

Inquire

Display a continuation prompt for this output.

Handle Warnings, Errors, and Terminating Errors

Problem

You want to handle warnings, errors, and terminating errors generated by scripts or other tools that you call.

Solution

To control how your script responds to warning messages, set the $warningPreference variable. In this example, to ignore them:

$warningPreference = "SilentlyContinue"

To control how your script responds to nonterminating errors, set the $errorActionPreference variable. In this example, to ignore them:

$errorActionPreference = "SilentlyContinue"

To control how your script responds to terminating errors, you can use either the try / catch / finally statements, or the trap statement. In this example, to output a message and continue with the script:

try
{
    1 / $null
}
catch [DivideByZeroException]
{
    "Don't divide by zero!"
}
finally
{
    "Script that will be executed even if errors occur in the try statement"
}

Use the trap statement if you want its error handling to apply to the entire scope:

trap [DivideByZeroException] { "Don't divide by zero!"; continue }
1 / $null

Discussion

PowerShell defines several preference variables that help you control how your script reacts to warnings, errors, and terminating errors. As an example of these error management techniques, consider the following script:

##############################################################################

Write-Warning "Warning: About to generate an error"
Write-Error "Error: You are running this script"
throw "Could not complete operation."      

For more information about running scripts, see the section called “Run Programs, Scripts, and Existing Tools”.

You can now see how a script might manage those separate types of errors:

PS > $warningPreference = "Continue"
PS > Get-WarningsAndErrors
WARNING: Warning: About to generate an error
Get-WarningsAndErrors : Error: You are running this script
At line:1 char:22
+ Get-WarningsAndErrors <<<<
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteError
   Exception
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorExc
   eption,Get-WarningsAndErrors

Could not complete operation.
At line:15 char:6
+ throw <<<<  "Could not complete operation."
    + CategoryInfo          : OperationStopped: (Could not complete opera
   tion.:String) [], RuntimeException
    + FullyQualifiedErrorId : Could not complete operation.

Once you modify the warning preference, the original warning message gets suppressed. A value of SilentlyContinue is useful when you are expecting an error of some sort.

PS > $warningPreference = "SilentlyContinue"
PS > Get-WarningsAndErrors
Get-WarningsAndErrors : Error: You are running this script
At line:1 char:22
+ Get-WarningsAndErrors <<<<
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteError
   Exception
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorExc
   eption,Get-WarningsAndErrors

Could not complete operation.
At line:15 char:6
+ throw <<<<  "Could not complete operation."
    + CategoryInfo          : OperationStopped: (Could not complete opera
   tion.:String) [], RuntimeException
    + FullyQualifiedErrorId : Could not complete operation.

When you modify the error preference, you suppress errors and exceptions, as well:

PS > $errorActionPreference = "SilentlyContinue"
PS > Get-WarningsAndErrors
PS > 

An addition to the $errorActionPreference variable, all cmdlets let you specify your preference during an individual call:

PS > $errorActionPreference = "Continue"
PS > Get-ChildItem IDoNotExist
Get-ChildItem : Cannot find path '...\IDoNotExist' because it does not exist.
At line:1 char:14
+ Get-ChildItem  <<<< IDoNotExist
PS > Get-ChildItem IDoNotExist -ErrorAction SilentlyContinue
PS > 

If you reset the error preference back to Continue, you can see the impact of a try / catch / finally statement. The message from the Write-Error call makes it through, but the exception does not:

PS > $errorActionPreference = "Continue"
PS > try { Get-WarningsAndErrors } catch { "Caught an error" }
Get-WarningsAndErrors : Error: You are running this script
At line:1 char:28
+ try { Get-WarningsAndErrors <<<<  } catch { "Caught an error" }
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteError
   Exception
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorExc
   eption,Get-WarningsAndErrors

Caught an error

The try / catch / finally statement acts like the similar statement in other programming languages. First, it executes the code inside of its script block. If it encounters a terminating error, it executes the code inside of the catch script block. It executes the code in the finally statement no matter what—an especially useful feature for cleanup or error-recovery code.

A similar technique is the trap statement:

PS > $errorActionPreference = "Continue"
PS > trap { "Caught an error"; continue }; Get-WarningsAndErrors
Get-WarningsAndErrors : Error: You are running this script
At line:1 char:60
+ trap { "Caught an error"; continue }; Get-WarningsAndErrors <<<<
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteError
   Exception
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorExc
   eption,Get-WarningsAndErrors

Caught an error

Unlike the try statement, the trap statement handles terminating errors for anything in the scope that defines it. For more information about scopes, see the section called “Control Access and Scope of Variables and Other Items”.

Note

After handling an error, you can also remove it from the system's error collection by typing $error.RemoveAt(0).

For more information about error management in PowerShell, see the section called “Managing Errors”. For more detailed information about the valid settings of these preference variables, see Appendix A, PowerShell Language and Environment.

Output Warnings, Errors, and Terminating Errors

Problem

You want your script to notify its caller of a warning, error, or terminating error.

##############################################################################

Write-Warning "Warning: About to generate an error"
Write-Error "Error: You are running this script"
throw "Could not complete operation."

Solution

To write warnings and errors, use the Write-Warning and Write-Error cmdlets, respectively. Use the throw statement to generate a terminating error.

Discussion

When you need to notify the caller of your script about an unusual condition, the Write-Warning, Write-Error, and throw statements are the way to do it. If your user should consider the message as more of a warning, use the Write-Warning cmdlet. If your script encounters an error (but can reasonably continue past that error), use the Write-Error cmdlet. If the error is fatal and your script simply cannot continue, use a throw statement.

For information on how to handle these errors when thrown by other scripts, see the section called “Handle Warnings, Errors, and Terminating Errors”. For more information about error management in PowerShell, see the section called “Managing Errors”. For more information about running scripts, see the section called “Run Programs, Scripts, and Existing Tools”.

Program: Analyze a Script's Performance Profile

When you write scripts that heavily interact with the user, you may sometimes feel that your script could benefit from better performance.

When tackling performance problems, the first rule is to measure the problem. Unless you can guide your optimization efforts with hard performance data, you are almost certainly directing your efforts to the wrong spots. Random cute performance improvements will quickly turn your code into an unreadable mess, often with no appreciable performance gain! Low-level optimization has its place, but it should always be guided by hard data that supports it.

The way to obtain hard performance data is from a profiler. PowerShell doesn't ship with a script profiler, but Example 15.4, “Get-ScriptPerformanceProfile.ps1” uses PowerShell features to implement one.

Example 15.4. Get-ScriptPerformanceProfile.ps1

################################################################################ 

param($logFilePath = $(throw "Please specify a path to the transcript log file.")) 

function Main 
{ 
    $uniqueLines = @{} 
    $samples = GetSamples $uniqueLines 
     
    "Breakdown by line:" 
    "----------------------------" 

    $counts = @{} 
    $totalSamples = 0; 
    foreach($item in $samples.Keys)  
    {  
       $counts[$samples[$item]] = $item  
       $totalSamples += $samples[$item] 
    } 

    foreach($count in ($counts.Keys | Sort-Object -Descending)) 
    { 
       $line = $counts[$count] 
       $percentage = "{0:#0}" -f ($count * 100 / $totalSamples) 
       "{0,3}%: Line {1,4} -{2}" -f $percentage,$line, 
          $uniqueLines[$line] 
    } 

    "" 
    "Breakdown by marked regions:" 
    "----------------------------" 
    $functionMembers = GenerateFunctionMembers 
     
    foreach($key in $functionMembers.Keys) 
    { 
        $totalTime = 0 
        foreach($line in $functionMembers[$key]) 
        { 
            $totalTime += ($samples[$line] * 100 / $totalSamples) 
        } 
         
        $percentage = "{0:#0}" -f $totalTime 
        "{0,3}%: {1}" -f $percentage,$key 
    } 
} 

function GetSamples($uniqueLines) 
{ 
    $logStream = [System.IO.File]::Open($logFilePath, "Open", "Read", "ReadWrite") 
    $logReader = New-Object System.IO.StreamReader $logStream 

    $random = New-Object Random 
    $samples = @{} 

    $lastCounted = $null 
     
    while(-not $host.UI.RawUI.KeyAvailable) 
    { 
       $sleepTime = [int] ($random.NextDouble() * 100.0) 
       Start-Sleep -Milliseconds $sleepTime 

       $rest = $logReader.ReadToEnd() 
       $lastEntryIndex = $rest.LastIndexOf("DEBUG: ") 

       if($lastEntryIndex -lt 0)  
       {  
          if($lastCounted) { $samples[$lastCounted] ++ } 
          continue;  
       } 
        
       $lastEntryFinish = $rest.IndexOf("\n", $lastEntryIndex) 
       if($lastEntryFinish -eq -1) { $lastEntryFinish = $rest.length } 

       $scriptLine = $rest.Substring(
            $lastEntryIndex, ($lastEntryFinish - $lastEntryIndex)).Trim() 
       if($scriptLine -match 'DEBUG:[ \t]*([0-9]*)\+(.*)') 
       { 
           $last = $matches[1] 
            
           $lastCounted = $last 
           $samples[$last] ++ 
            
           $uniqueLines[$last] = $matches[2] 
       } 

       $logReader.DiscardBufferedData() 
    } 

    $logStream.Close() 
    $logReader.Close() 
     
    $samples 
} 

function GenerateFunctionMembers 
{ 
    $callstack = New-Object System.Collections.Stack 
    $currentFunction = "Unmarked" 
    $callstack.Push($currentFunction) 

    $functionMembers = @{} 

    foreach($line in (Get-Content $logFilePath)) 
    { 
        if($line -match 'write-debug "ENTER (.*)"') 
        { 
            $currentFunction = $matches[1] 
            $callstack.Push($currentFunction) 
        } 
        elseif($line -match 'write-debug "EXIT"') 
        { 
            [void] $callstack.Pop() 
            $currentFunction = $callstack.Peek() 
        } 
        else 
        { 
            if($line -match 'DEBUG:[ \t]*([0-9]*)\+') 
            { 
                if(-not $functionMembers[$currentFunction]) 
                { 
                    $functionMembers[$currentFunction] = 
                        New-Object System.Collections.ArrayList 
                } 
                 
                if(-not $functionMembers[$currentFunction].Contains($matches[1])) 
                { 
                    [void] $functionMembers[$currentFunction].Add($matches[1]) 
                } 
            } 
        } 
    } 
     
    $functionMembers 
} 

. Main

      

3 comments

  1. AndrewTearle Posted 16 days and 8 hours ago

    Get-ScriptPerformanceProfile.ps1 Looks really useful - some expanded explanation would be welcome.

  2. David "Makovec" Moravec Posted 11 days and 11 hours ago

    I would also vote for description. I expect that in the final version all scripts will be with comments as first edition. For example this script is over five pages with very useful comments inside (in 1st ed.).

  3. Lee Holmes Posted 10 days and 12 hours ago

    Sorry, there's a bug in the feedback system that's dropping comments from most of the book. It's in the manuscript and will be in print - hopefully we can get the online issues sorted out.

Add a comment


For more information about running scripts, see the section called “Run Programs, Scripts, and Existing Tools”.

You must sign in or register before commenting
*
*
*
*
*

Atom Icon Comments on this page or Comments on the whole book.