24 Handling errors

In this chapter, we’ll focus on how to capture, deal with, log, and otherwise handle errors the tool may encounter.

Note PowerShell.org offers a free e-book called The Big Book of PowerShell Error Handling, which dives into this topic from a more technical reference perspective, at https://devopscollective.org/ebooks/. We recommend checking it out once you’ve completed this tutorial-focused chapter.

Before we get started, there are two variables that we need to get comfortable with. The first is the $Error automation variable. This contains an array of error objects that have occurred in your current session, with the most recent error object showing up at $Error[0]. By default, all errors will be put into this variable. You can change this behavior by setting your ErrorAction common parameter to Ignore. You can get more information about automatic variables by running get-help about_automatic_variables.

The second built-in variable that you can use is the common parameter variable ErrorVariable. This is an object that you can send errors to, so you can use them at a later time if needed (e.g., for writing to a log file):

New-PsSession -ComputerName SRV01 -ErrorVariable a

ErrorVariable will only hold the most recent error unless you add a + (plus) sign in front of it:

New-PsSession -ComputerName SRV01 -ErrorVariable +a

NOTE We did not use the $ in front of the error variable because it is not needed here.

24.1 Understanding errors and exceptions

PowerShell defines two broad types of bad situations: an error and an exception. Because most PowerShell commands are designed to deal with multiple things at once, and because in many cases a problem with one thing doesn’t mean you want to stop dealing with all the other things, PowerShell tries to err on the side of “just keep going.” So, often, when something goes wrong in a command, PowerShell will emit an error and keep going (figure 24.1). For example:

Get-Service -Name BITS,Nobody,WinRM

Figure 24.1 Get-Service with a service that doesn’t exist

The service Nobody doesn’t exist, so PowerShell will emit an error on that second item. But by default, PowerShell will keep going and process the third item in the list. When PowerShell is in this keep-going mode, you can’t have your code respond to the problem condition. If you want to do something about the problem, you have to change PowerShell’s default response to this kind of nonterminating error.

At a global level, PowerShell defines an $ErrorActionPreference variable, which tells PowerShell what to do in the event of a nonterminating error—that is, this variable tells PowerShell what to do when a problem comes up, but PowerShell is able to keep going. The default value for this variable is Continue. Here are the options:

  • Break—Enter the debugger when an error occurs or when an exception is raised.

  • Continue (default)—Displays the error message and continues executing.

  • Ignore—Suppresses the error message and continues to execute the command. The Ignore value is intended for per-command use, not for use as a saved preference. Ignore isn’t a valid value for the $ErrorActionPreference variable.

  • Inquire—Displays the error message and asks you whether you want to continue.

  • SilentlyContinue—No effect. The error message isn’t displayed, and execution continues without interruption.

  • Stop—Displays the error message and stops executing. In addition to the error generated, the Stop value generates an ActionPreferenceStopException object to the error stream.

  • Suspend—Automatically suspends a workflow job to allow for further investigation. After investigation, the workflow can be resumed. The Suspend value is intended for per-command use, not for use as a saved preference. Suspend isn’t a valid value for the $ErrorActionPreference variable.

Rather than changing $ErrorActionPreference globally, you’ll typically want to specify a behavior on a per-command basis. You can do this using the -ErrorAction common parameter, which exists on every PowerShell command—even the ones you write yourself that include [CmdletBinding()]. For example, try running these commands, and note the different behaviors:

Get-Service -Name Foo,BITS,Nobody,WinRM -ErrorAction Continue
Get-Service -Name BITS,Nobody,WinRM -ErrorAction SilentlyContinue
Get-Service -Name BITS,Nobody,WinRM -ErrorAction Inquire
Get-Service -Name BITS,Nobody,WinRM -ErrorAction Ignore
Get-Service -Name BITS,Nobody,WinRM -ErrorAction Stop

The thing to remember is that you can’t handle exceptions in your code unless PowerShell actually generates an exception. Most commands won’t generate an exception unless you run them with the Stop error action. One of the biggest mistakes people make is forgetting to add -EA Stop to a command where they want to handle the problem (-EA is short for -ErrorAction).

24.2 Bad handling

We see people engage in two fundamentally bad practices. These aren’t always bad, but they’re usually bad, so we want to bring them to your attention.

First up is globally setting the preference variable right at the top of a script or function:

$ErrorActionPreference='SilentlyContinue' 

In the olden days of VBScript, people used On Error Resume Next. This is essentially saying, “I don’t want to know if anything is wrong with my code.” People do this in a misguided attempt to suppress possible errors that they know won’t matter. For example, attempting to delete a file that doesn’t exist will cause an error—but you probably don’t care, because mission accomplished either way, right? But to suppress that unwanted error, you should be using -EA SilentlyContinue on the Remove-Item command, not globally suppressing all errors in your script.

The other bad practice is a bit more subtle and can come up in the same situation. Suppose you do run Remove-Item with -EA SilentlyContinue, and then suppose you try to delete a file that does exist but that you don’t have permission to delete. You’ll suppress the error and wonder why the file still exists.

Before you start suppressing errors, make sure you’ve thought it through. Nothing is more vexing than spending hours debugging a script because you suppressed an error message that would have told you where the problem was. We can’t tell you how often this comes up in forum questions.

24.3 Two reasons for exception handling

There are two broad reasons to handle exceptions in your code. (Notice that we’re using their official name, exceptions, to differentiate them from the nonhandleable errors that we wrote about previously.)

Reason one is that you plan to run your tool out of your view. Perhaps it’s a scheduled task, or maybe you’re writing tools that will be used by remote customers. In either case, you want to make sure you have evidence for any problems that occur, to help you with debugging. In this scenario, you might globally set $ErrorActionPreference to Stop at the top of your script, and wrap the entire script in an error-handling construct. That way, any errors, even unanticipated ones, can be trapped and logged for diagnostic purposes. Although this is a valid scenario, it isn’t the one we’re going to focus on in this book.

We’ll focus on reason two—you’re running a command where you can anticipate a certain kind of problem occurring, and you want to actively deal with that problem. This might be a failure to connect to a computer, a failure to log on to something, or another scenario along those lines. Let’s dig into that.

24.4 Handling exceptions

Suppose you are building a script that connects to remote machines. You can anticipate the New-PSSession command running into problems: a computer might be offline or nonexistent, or the computer might not work with the protocol you’ve selected. You want to catch those conditions and, depending on the parameters you ran with, log the failed computer name to a text file and/or try again using the other protocol. You’ll start by focusing on the command that could cause the problem and make sure it’ll generate a terminating exception if it runs into trouble. Change this:

$computer = 'Srv01'
Write-Verbose "Connecting to $computer"
$session = New-PSSession -ComputerName $computer

to this:

$computer = 'Srv01'
Write-Verbose "Connecting to $computer"
$session = New-PSSession -ComputerName $computer -ErrorAction Stop

But what if we want to run this command on multiple computers? We have two options. The first option is to put multiple computer names into the $computer variable. After all, it does accept an array of strings.

$computer = 'Srv01','DC01','Web02'
Write-Verbose "Connecting to $computer"
$session = New-PSSession -ComputerName $computer -ErrorAction Stop

Here is where you will need to make some personal decisions. Do you want to let your script continue to run if an error occurs and capture the error for later use, or do you want your script to stop running immediately? A lot of this will depend on what you are trying to accomplish. If you are attempting to connect to five remote computers to run a command, is it okay if it runs on only four of them, and you log the error that the fifth computer could not be contacted, or do you need the command to run on either all five or none of them?

You have two options here. The first option is to wrap your command in a foreach loop. That way ErrorAction is set each time the command is executed. If you have one failure, the rest of the sessions will still be created. This, however, negates the fact that New-PSSession computername parameter can take an array of objects as its input:

foreach ($computer in $computername) {
          Write-Verbose "Connecting to $computer"
     $session = New-PSSession -ComputerName $Computer -ErrorAction Stop
        }

The second option is to tell PowerShell to continue and put the error in the ErrorVariable common parameter (don’t forget to append the + symbol to the existing variable data):

$computer = 'Srv01','DC01','Web02'
   $session = New-PSSession -ComputerName $Computer -ErrorVariable a

Make sure you understand why this design principle is so important! As we mentioned before, we do not want to suppress useful errors if we can help it.

Try it now Using what you have learned so far in this chapter and in previous chapters, get the state of the spooler service and the print service. Make sure to log your errors.

Just changing the error action to Stop isn’t enough, though. You also need to wrap your code in a Try/Catch construct. If an exception occurs in the Try block, then all the subsequent code in the Try block will be skipped, and the Catch block will execute instead:

try { blahfoo }
catch { Write-Warning “Warning: An error occurred." }

Here’s what’s happening: within the Catch block, you take the opportunity to write out a warning message for the benefit of the user. They can suppress warnings by adding -Warning-Action SilentlyContinue when running the command. This is some complex logic—go through it a few times, and make sure you understand it!

24.5 Handling exceptions for noncommands

What if you’re running something—like a .NET Framework method—that doesn’t have an -ErrorAction parameter? In most cases, you can run it in a Try block as is, because most of these methods will throw trappable, terminating exceptions if something goes wrong. The nonterminating exception thing is unique to PowerShell commands like functions and cmdlets.

But you still may have instances when you need to do this:

Try {
    $ErrorActionPreference = "Stop"
    # run something that doesn't have -ErrorAction
    $ErrorActionPreference = "Continue"
} Catch {
    # ...
}

This is your error handling of last resort. Basically, you’re temporarily modifying $ErrorActionPreference for the duration of the one command (or whatever) for which you want to catch an exception. This isn’t a common situation in our experience, but we figured we’d point it out.

24.6 Going further with exception handling

It’s possible to have multiple Catch blocks after a given Try block, with each Catch dealing with a specific type of exception. For example, if a file deletion failed, you could react differently for a File Not Found or an Access Denied situation. To do this, you’ll need to know the .NET Framework type name of each exception you want to call out separately. The Big Book of PowerShell Error Handling has a list of common ones and advice for figuring these out (e.g., generating the error on your own in an experiment, and then figuring out what the exception type name was). Broadly, the syntax looks like this:

Try {
    # something here generates an exception
} Catch [Exception.Type.One] {
    # deal with that exception here
} Catch [Exception.Type.Two] {
    # deal with the other exception here
} Catch {
    # deal with anything else here
} Finally {
    # run something else
}

Also shown in that example is the optional Finally block, which will always run after the Try or the Catch, whether or not an exception occurs.

Deprecated exception handling

You may, in your internet travels, run across a Trap construct in PowerShell. This dates back to v1, when the PowerShell team frankly didn’t have time to get Try/Catch working, and Trap was the best short-term fix they could come up with. Trap is deprecated, meaning it’s left in the product for backward compatibility, but you’re not intended to use it in newly written code. For that reason, we’re not covering it here. It does have some uses in global, “I want to catch and log any possible error” situations, but Try/Catch is considered a more structured, professional approach to exception handling, and we recommend that you stick with it.

24.7 Lab

Using what you have learned so far, do the following:

  • Create a function that will get the uptime on remote machines. Make sure you are using the built-in commands in PowerShell 7 and not .NET methods.

  • Make sure the function can accept input for multiple machines.

  • Include error-handling methods that we discussed in this chapter such as Try/ Catch and error actions.

Above and beyond

Take what you have learned so far about remoting and make your function work regardless of the operating system. Here is a hint: There are three built-in variables that may prove useful:

$IsMacOS
$IsLinux
$IsWindows

Here are some key things to remember:

  • $Error contains all the error messages in your session.

  • ErrorVariable can be used to store errors as well (append the + sign to it).

  • Try/Catch is your friend, but only with nonterminating errors.

24.8 Lab answer

Function Get-PCUpTime {
    param (
        [string[]]$ComputerName = 'localhost'
    )
    try {
        foreach ($computer in $computerName) {
            If ($computer -eq "localhost") {
                Get-Uptime
            }
            Else { Invoke-command -ComputerName $computer -ScriptBlock 
           { Get-Uptime } -ErrorAction Stop}
        }
    }
    catch {
        Write-Error "Cannot connect To $computer"
    }
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.169.40