PowerShell in Depth, Second Edition (2015)
Part 3. PowerShell scripting and automation
Chapter 30. Error handling techniques
This chapter covers
· Error handling parameters and variables
· Trap
· Try...Catch...Finally
You’ll inevitably write a script that contains a command that fails. Failure is often something you can anticipate: A computer isn’t online or reachable, a file can’t be found, access is denied, and other similar conditions are all ones that you expect from time to time. PowerShell enables you to catch those errors and handle them in your own way. For example, you might want to log a failed computer name to a file for retrying, or you might want to write an entry to the Windows event log when access is denied. Error handling is the process of identifying these conditions, catching the error, and doing something about it while continuing your processes on other machines.
The point of this chapter isn’t just to teach you how to catch errors but rather how to handle errors gracefully. A simple script that fails when it encounters an error is handling it, but not very well if it fails on the first computer of 30, which means you have to restart the script. This is especially bad news if the script ran as an overnight activity. Proper application of error handling techniques enables you to be more productive. Which do you want your script to report: “I did 299 out of 300 machines and failed on this one” or “It failed”?
30.1. About errors and exceptions
Let’s begin by defining a few terms. These aren’t necessarily official PowerShell terms, but they’re useful for describing PowerShell’s behavior, so we’ll go ahead and use them.
The first word is error. An error in PowerShell is a message that’s displayed on screen when something goes wrong. By default, PowerShell displays its errors in red text on a black background (in the console host, that is; the ISE uses red text).
Tip
If you find the red on black difficult to read (many folks do), or if it takes you back to high school English class and red-penned essays, you can change the color. Don prefers green on black; he says it’s easy to read and makes him feel like he’s done something right:$host.PrivateData.ErrorForegroundColor = 'green' will do the trick. This resets in every new session, so add it to a profile script if you want it to be permanent.
An exception is a specific kind of object that results from an error. Specifically, an exception forces PowerShell to see if you’ve implemented some routine that should run in response to the error. With an error, you’re just getting the red text; with an exception handling routine, you’re getting the chance to do something about it. That’s what this chapter is all about.
30.2. Using $ErrorActionPreference and –ErrorAction
PowerShell commands can encounter two types of error conditions: terminating and nonterminating. A terminating error is PowerShell saying, “There’s no way I can possibly continue—this party is over.” A nonterminating error says, “Something bad happened, but I can try to keep going.” For example, suppose you ask PowerShell to retrieve some WMI information from 10 computers. Computer number five fails—perhaps it isn’t online at the time. That’s bad, but there’s no reason PowerShell can’t continue with computers six, seven, and so on—and it will, by default, because that’s a nonterminating error.
You can use a shell-wide setting in $ErrorActionPreference, a built-in variable (technically, a preference variable—see about_Preference_Variables for more information on preference variables) to tell a command what to do when a nonterminating error pops up.$ErrorActionPreference offers these four settings:
· Inquire—Ask the user what to do. You’ll probably never do this except when debugging a command because it’s pretty intrusive. With scheduled or other unattended scripts, it’s totally impractical.
· Continue—The default setting, this tells PowerShell to display an error message and keep going.
· SilentlyContinue—The setting you wish your kids had, this tells PowerShell not only to keep going but to not display any error messages.
· Stop—This forces the nonterminating error to become a terminating exception. That means you can catch the exception and do something about it.
Unfortunately, a great many PowerShell users think it’s okay to put this right at the top of their scripts:
$ErrorActionPreference = 'SilentlyContinue'
In the days of VBScript, On Error Resume Next had the same effect as this command. This sends us into a rage, and not a silent one. Adding this to the top of a script effectively suppresses every single error message. What’s the author trying to hide here? We understand why people do this: They think, “Well, the only thing that can go wrong is the thing I don’t care about, like a computer not being available, so I’ll just hide the error message.” The problem is that other errors can crop up, and by hiding the error message you’ll have a much harder time detecting, debugging, and solving those errors. So please don’t ever put that line in your scripts. In fact, you’ll rarely have to mess with $ErrorActionPreference at all.
Every single PowerShell cmdlet supports a set of common parameters. Look at the help for any cmdlet and you’ll see <CommonParameters> at the end of the parameter list. Run help about_common_parameters to see the list of common parameters. One of them of particular interest to us right now is –ErrorAction, which can be abbreviated as –EA. This parameter lets you specify an error action for just that command. Essentially, it’s as if you set $ErrorActionPreference to something, ran the command, and then put $ErrorActionPreference back the way you found it. The parameter accepts the same four values as $ErrorActionPreference. Let’s see them in action. This example has PowerShell attempt to display the contents of two files, one that exists and one that doesn’t. Start with Inquire:
PS C:\> Get-Content -Path good.txt,bad.txt -ErrorAction Inquire
Confirm
Cannot find path 'C:\bad.txt' because it does not exist.
[Y] Yes [A] Yes to All [H] Halt Command [S] Suspend [?] Help
(default is "Y"):y
Get-Content : Cannot find path 'C:\bad.txt' because it does not exist
.
At line:1 char:12
+ Get-Content <<<< -Path good.txt,bad.txt -ErrorAction Inquire
+ CategoryInfo : ObjectNotFound: (C:\bad.txt:String) [G
et-Content], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Comm
ands.GetContentCommand
This is the content from the file that exists
As you can see, the code prompts you to continue when it runs into the bad file. You say “Y” to continue, and it goes on to the second file, displaying its contents. Now let’s look at Continue:
PS C:\> Get-Content -Path good.txt,bad.txt -ErrorAction Continue
Get-Content : Cannot find path 'C:\bad.txt' because it does not exist
.
At line:1 char:12
+ Get-Content <<<< -Path good.txt,bad.txt -ErrorAction Continue
+ CategoryInfo : ObjectNotFound: (C:\bad.txt:String) [G
et-Content], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Comm
ands.GetContentCommand
This is the content from the file that exists
You get the same basic effect, only this time without the prompt. You get an error, followed by the content of the file that existed. Now look at SilentlyContinue:
PS C:\> Get-Content -Path good.txt,bad.txt -ErrorAction SilentlyContinue
This is the content from the file that exists
There’s no error, just the content from the file that existed. Finally, we’ll look at Stop:
PS C:\> Get-Content -Path good.txt,bad.txt -ErrorAction Stop
Get-Content : Cannot find path 'C:\bad.txt' because it does not exist
.
At line:1 char:12
+ Get-Content <<<< -Path good.txt,bad.txt -ErrorAction Stop
+ CategoryInfo : ObjectNotFound: (C:\bad.txt:String) [G
et-Content], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Comm
ands.GetContentCommand
This error action prevents PowerShell from going on to the second file. When PowerShell hits an error, it stops, exactly as you told it to do. This generated a trappable (or catchable) exception, although you haven’t put anything in place to deal with it, so you still get the error message. We’ll cover handling these caught exceptions in a bit.
30.3. Using –ErrorVariable
Another one of the common parameters is –ErrorVariable, or –EV for short. This parameter accepts the name of a variable (remember, the name doesn’t include the $ symbol), and if the command generates an error, it’ll be placed into that variable. Using this parameter is a great way to see which error occurred and perhaps take different actions for different errors. The neat thing is that it’ll grab the error even if you set –ErrorAction to SilentlyContinue, which suppresses the output of the error on the screen:
PS C:\> Get-Content good.txt,bad.txt -EA SilentlyContinue -EV oops
This is the content from the file that exists
PS C:\> $oops
Get-Content : Cannot find path 'C:\bad.txt' because it does not exist
.
At line:1 char:12
+ Get-Content <<<< good.txt,bad.txt -EA SilentlyContinue -EV oops
+ CategoryInfo : ObjectNotFound: (C:\bad.txt:String) [G
et-Content], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Comm
ands.GetContentCommand
As you can see, this code specified oops as the variable name, and after running the command you can display the error by accessing $oops.
30.4. Using $?
The $? variable is a way to tell if your last command succeeded. PowerShell has a number of variables that it automatically creates for you. You’ve already met some of them and will meet more in this chapter. We recommend that you read the help file about_Automatic_Variables to discover the full suite and what you can do with them.
One such automatic variable is $?. It stores the execution status of the last operation you performed in PowerShell. The status is stored as a Boolean value and will be set to True if the operation succeeded and False if it failed. You can see the $? automatic variable in use as you try this code:
PS C:\> Get-Process powershell
Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName
------- ------ ----- ----- ----- ------ -- -----------
573 45 273040 277928 778 9.84 3920 powershell
PS C:\> $?
True
The command succeeded so the value of $? is set to True. Now, try a command that’ll fail:
PS C:\> Get-Pracess powershell
Get-Pracess : The term 'Get-Pracess' is not recognized as
the name of a cmdlet, function, script file, or operable
program. Check the spelling of the name, or if a path was
included, verify that the path is correct and try again.
At line:1 char:1
+ Get-Pracess powershell
+ ~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound:
(Get-Pracess:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
PS C:\> $?
False
Get-Pracess doesn’t exist so the command fails and $? is set to False. You can use $? to test an action and determine what to do next:
$proc = Get-Process notepad -ErrorAction SilentlyContinue
if ($?) {
Stop-Process -InputObject $proc
}
else {
Write-Warning -Message "Notepad not running"
}
An attempt is made to get the process associated with notepad.exe. If it succeeds($? = True), the process is stopped. If the notepad process isn’t running, a warning to that effect is issued.
Knowing whether or not a command worked is useful, but to take your error handling to the next level, you need to know the type of error that occurred.
30.5. Using $Error
In addition to using the common –ErrorVariable, which is completely optional, you can find recent exception objects in the variable $Error, which is another PowerShell automatic variable. Whenever an exception occurs, it’s added to $Error. By default the variable holds the last 256 errors. The maximum number of errors is controlled by another preference variable, $MaximumHistoryCount:
PS C:\> $MaximumErrorCount
256
PS C:\> $MaximumErrorCount=512
Now this PowerShell session will keep track of the last 512 exceptions. If you want to always use this setting, put this command in your profile.
The $Error variable is an array where the first element is the most recent exception. As new exceptions occur, the new one pushes the others down the list. Once the maximum count is reached, the oldest exception is discarded. If you wanted to revisit the last error, you’d do this:
PS C:\> $error[0]
Get-WmiObject : Invalid class "win32_bis"
+ CategoryInfo : InvalidType: (:) [Get-WmiObject], ManagementException
+ FullyQualifiedErrorId : GetWMIManagementException, Microsoft.PowerShell.Commands.GetWmiObjectCommand
We should point out that this is an object. You can pipe it to Get-Member to learn more:
PS C:\> $error[0] | Get-Member
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.R...
GetType Method type GetType()
ToString Method string ToString()
writeErrorStream NoteProperty System.Boolean writeErrorStream=True
CategoryInfo Property System.Management.Automation.ErrorC...
ErrorDetails Property System.Management.Automation.ErrorD...
Exception Property System.Exception Exception {get;}
FullyQualifiedErrorId Property string FullyQualifiedErrorId {get;}
InvocationInfo Property System.Management.Automation.Invoca...
PipelineIterationInfo Property System.Collections.ObjectModel.Read...
ScriptStackTrace Property string ScriptStackTrace {get;}
TargetObject Property System.Object TargetObject {get;}
PSMessageDetails ScriptProperty System.Object PSMessageDetails {get...
Some of these properties are nested objects, such as Exception:
PS C:\> $error[0].exception | Get-Member
TypeName: System.Management.ManagementException
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetBaseException Method System.Exception GetBaseException()
GetHashCode Method int GetHashCode()
GetObjectData Method System.Void GetObjectData(System.Runtime.Ser...
GetType Method type GetType()
ToString Method string ToString()
Data Property System.Collections.IDictionary Data {get;}
ErrorCode Property System.Management.ManagementStatus ErrorCode...
ErrorInformation Property System.Management.ManagementBaseObject Error...
HelpLink Property string HelpLink {get;set;}
InnerException Property System.Exception InnerException {get;}
Message Property string Message {get;}
Source Property string Source {get;set;}
StackTrace Property string StackTrace {get;}
TargetSite Property System.Reflection.MethodBase TargetSite {get;}
When you get to trapping and catching exceptions, this object is passed to your error handler, so understanding that the exception is an object is important. For example, you might want to display the following error message:
PS C:\> Write-Host "Command failed with error message
$($error[0].exception.message)" -ForegroundColor Yellow
Command failed with error message Invalid class "win32_bis"
You can use the common –ErrorVariable to capture exception objects on a per-cmdlet basis or look at the $Error variable to work with the most recent exceptions. But ideally, especially when scripting, you’ll want to handle exceptions more gracefully.
30.6. Trap constructs
The Trap construct was introduced in PowerShell v1. It’s not an awesome way of handling errors, but it’s the best Microsoft could get into the product and still hit their shipping deadline. It’s effective but it can be confusing, especially because it wasn’t documented in the help files. These days, almost everyone prefers the newer, and more versatile, Try...Catch...Finally construct, but because Trap still exists and still works, we wanted to take the time to explain.
Whenever a trappable exception occurs (meaning a terminating error, or a nonterminating one that was made terminating by the –ErrorAction Stop parameter), PowerShell will jump back in your script and execute a Trap construct if it has encountered one by then. In other words, yourTrap construct has to be defined before the error happens; PowerShell won’t scan ahead to look for one. For example, start with the following script.
Listing 30.1. Trap.ps1, demonstrating the use of the Trap construct
trap {
Write-Host "Trapping..."
"Error!" | Out-File c:\errors.txt
continue
}
Write-Host "Starting..."
Get-Content good.txt,bad.txt -EA Stop
Write-Host "Finishing..."
In listing 30.1, you’re using Write-Host mainly to give you some output to follow the flow of this; that’s going to become important in a minute. Right now, running the script displays the following output:
PS C:\> C:\trap.ps1
Starting...
Trapping...
Finishing...
You can use that output to follow what happened. Once the error occurred—and you made sure to turn it into a trappable exception by specifying –ErrorAction Stop—PowerShell handed off control to the Trap construct. You ended with the Continue statement, which tells the shell to go back and pick up on the line after the one that caused the exception.
How you end a Trap construct is crucial. You have two choices:
· Continue—Tells the shell to stay within the same scope as the Trap construct and resume execution on the line that follows the command that caused the error
· Break—Tells the shell to exit the current scope, passing the exception up to the parent scope
This scope business is one reason why Trap is so complex. Consider the following listing, which revises the script significantly. You’re adding a function, which is its own scope, and installing a Trap construct inside it.
Listing 30.2. Trap.ps1, demonstrating the flow of scope for trapping
The script in listing 30.2 starts with the first Write-Host command because the trap and function defined earlier in the code haven’t been called yet. It then calls the function , so execution proceeds to the Try-This function. An error occurs , so PowerShell looks for a trap within the current scope. It finds one and executes it. The trap exits with Continue, so the shell stays within the same scope and finishes the function . The function exits naturally, allowing the script to continue and wrap up . The Trap defined within the script never executes. The output looks like this:
Starting the script...
Starting the function...
Trapping at function scope...
Ending the function...
Finishing the script...
Now look at the next listing, which makes only one change, which we’ve boldfaced.
Listing 30.3. Trap.ps1, with one change: how the function’s trap ends
All you’re doing in listing 30.3 is changing the way the function’s trap exits, but it’s going to significantly affect the output. By changing Continue to Break , you’re telling PowerShell to exit the function’s scope and pass the error with it. That’ll force the parent scope to look for a trap and execute the one it finds. The output looks like this:
Starting the script...
Starting the function...
Trapping at function scope...
Trapping at script scope...
Finishing the script...
As you can see, because you exited the function’s scope, the Ending the function line never got to execute. The script “saw” the error as occurring on the Try-This line, ran its trap, and then continued with Finishing the script....
You can also set up multiple traps, each one designed to handle a different exception:
trap {
"Other terminating error trapped"
}
trap [System.Management.Automation.CommandNotFoundException] {
"Command error trapped"
}
To set this up, you’ll need to know the .NET Framework class of a specific exception, such as [System.Management.Automation.CommandNotFoundException]. That can be tricky, and you probably won’t run across this technique much, but we wanted to make sure you knew what it was in case you do see someone using it.
Tip
Examine any error messages carefully when developing your script. If you look at the error messages at the start of section 30.3, you’ll see ItemNotFoundException. The exception that has caused the error is often given in PowerShell’s error messages. You can search MSDN for the full .NET class name of the exception.
Following this chain of scope, traps, and so forth can be difficult, especially when you’re trying to debug a complex, multilevel script. We think you need to be aware of this technique and how it behaves, because you’re likely to run across folks who still use it in the examples they write online. You’ll also run across older examples that use this, and we don’t think the Trap construct alone should put you off. But we don’t recommend using this construct; the newer Try...Catch...Finally is much better.
30.7. Try...Catch...Finally constructs
PowerShell v2 introduced this new, improved error handling construct. It looks and works a lot more like the error handling constructs in high-level languages like Visual Basic and C# or other languages such as T-SQL. You build a construct with two or three parts:
· The Try part contains the command or commands that you think might cause an error. You have to set their –ErrorAction to Stop in order to catch the error.
· The Catch part runs if an error occurs within the Try part.
· The Finally part runs whether or not an error occurred.
You must have the Try part, and you can choose to write a Catch, a Finally, or both. You must have at least one Catch or Finally block. A simple example looks like this:
Try {
Get-Content bad.txt,good.txt -EA Stop
} Catch {
Write-Host "Uh-oh!!"
}
This produces the following output:
PS C:\> C:\test.ps1
Uh-oh!!
There’s an important lesson here: If a command is trying to do more than one thing and one of them causes an error, the command stops. You’ll be able to catch the error, but you have no way to make the command go back and pick up where it left off. In this example, trying to read Bad.txt caused an error, and so Good.txt was never even attempted. Keep in mind that your commands should try to do only one thing at a time if you think one thing might cause an error that you want to trap. For example, you can pull your filenames out into an array and then enumerate the array so that you’re attempting only one file at a time:
$files = @('bad.txt','good.txt')
foreach ($file in $files) {
Try {
Get-Content $file -EA Stop
} Catch {
Write-Host "$file failed!!!"
}
}
This produces the following output:
PS C:\> C:\test.ps1
bad.txt failed!!!
This is the content from the file that exists
That’s the pattern you’ll generally see people use: Try one thing at a time, Catch the error, and rely on the Foreach construct to loop back around and pick up the next thing to try. This approach will make your scripts more verbose, but the robustness and stability introduced by usingTry...Catch...Finally is worth the extra effort.
Like the Trap construct, this also lets you catch specific errors. Again, we don’t always see people do this a lot, but it’s a good way to specify different actions for different types of errors, perhaps handling “file not found” differently than “access denied.” Here’s an example, from the about_try_catch_finally help file:
try
{
$wc = new-object System.Net.WebClient
$wc.DownloadFile("http://www.contoso.com/MyDoc.doc")
}
catch [System.Net.WebException],[System.IO.IOException]
{
"Unable to download MyDoc.doc from http://www.contoso.co
}
catch
{
"An error occurred that could not be resolved."
}
The first Catch block is catching two specific errors, a WebException and an IOException. The final Catch block is the catch-all and will catch any exceptions that haven’t been caught by a previous block. When using multiple Catch blocks, you must ensure that the Catch blocks for the most specific exceptions occur before more generic ones. PowerShell will use the first Catch block it has that can process the exception that has occurred.
When an exception is caught, the exception object is piped to the Catch block. This means you can incorporate the object into whatever code you want to execute. The exception object can be referenced using $_ because it’s a piped object. For example, you might use this for the second script block:
catch
{
$msg=("An error occurred that could not be resolved: {0}" –f $_.Exception.Message)
Write-Warning $msg
#Write the exception to a log file
$_.Exception | Select * | Out-file myerrors.txt –append
#Export the error to XML for later diagnosis
$_ | Export-Clixml UnknownWebException.xml
}
In this version, you’re referencing the exception object using $_ to write a warning message, log some information to a text file, and export the complete exception to an XML file. Later you could reimport the file to re-create the exception object and try to figure out what went wrong.
As you can see from the preceding code, you almost always need at least one Catch block for every Try, and they must be in sequence. Code in a Finally block runs regardless of whether there was an error. For example, you might use a Finally block to clean up connections or close files. The Finally block is completely optional.
One of the reasons we think Try...Catch...Finally is better than Trap is because there’s no jumping back and forth in your script. An error occurs, and you handle it right then and there in a linear, easy-to-follow fashion. You can use the two techniques in the same script, though. For example, you might use Try...Catch for the errors you can anticipate and want to handle, and use Trap to grab any other errors that pop up. We tend to steer clear of Trap, though, because it makes following the script’s execution flow much more difficult.
30.8. Summary
Handling errors in PowerShell scripts can be straightforward, particularly with the newer Try...Catch...Finally construct. Error handling adds a level of sophistication and professionalism to your scripts, dealing with errors gracefully rather than spewing a screenful of red text in front of your script’s users. When running scheduled and unattended scripts, error handling can be crucial to capturing errors (perhaps in a log file) for later review. We’re not implying that writing effective error handling is easy, but it’s essential if you want to create robust PowerShell tools.