Eric's Technical Outlet

Learning the hard way so you don't have to

Change the Default Error Action to Improve Your PowerShell Scripts’ Usability

Have you ever run a PowerShell script and gotten a wall of red text? Maybe even scrolling red text? If you’re a grizzled scripting veteran, you probably already know how to start troubleshooting that. But, did it really need to subject you to all of that? What about scripting newcomers? Does the PowerShell world really need to behave that way? Fortunately, we have a vastly superior alternative.

How to Change PowerShell’s Default Error Action

At the top of every single script that you write, after any comment-based help and param() block, within the begin{} block if you use one, add the following line:

$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

A contextualized example:

<#
.DESCRIPTION
    Script description.
#&gt;
param(
    [Parameter][String]$AStringParam
)

begin {
    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
}

process{
    # do work
}

PowerShell does have the ability to parse the text equivalent of the parameter, so you can shorten it. I strongly urge you to use the fully-qualified version above. I will explain why in a forthcoming article. The shortened version:

$ErrorActionPreference = 'Stop'

Impact of Changing PowerShell’s Default Error Action

By default, PowerShell will “Continue” when it encounters an error. That means that it will write the contents of the error into the error stream (typically shows up as red text in a console) and continue on with whatever it was doing. By changing it to “Stop”, everything halts at the first error.

The lasting impact depends on where you make the change:

  • Changing the default within a script only modifies it for that script. Once the script exits, the environment returns to its previous setting.
  • Changing the default within an interactive session will change it only for that session. New interactive prompts will use the system or profile default.
  • You can make the change permanent by adding it to one of the profiles. Read up on the various profiles and how to modify them on the Hey, Scripting Guy! blog.

The Practical Value of Stopping on Errors

I have four primary reasons for making this change in all scripts that I write:

  1. Predictability, part 1: You know that all non-trivial and most trivial scripts have a chance to fail. You cannot always know in advance where a script might fail. Without that knowledge, you face an unnecessary challenge in preventing cascade failures.
  2. Predictability, part 2: The documentation for the Continue option specifically states that it will “handle this event as normal”. What does “normal” mean? I obviously have not seen every single outcome of a Continued error, but I can use common cmdlets to demonstrate that a try block will not send an exception into the catch block from a cmdlet with a Continue error action. When you set it to Stop, try/catch blocks behave as expected.
  3. Cascade failures: We often use the outcome of one statement in future statements. For instance, you open a file, operate on the file, then close the file. If opening the file fails, the default of Continue will also cause the file operation to fail and then it will cause the file close to fail. Even worse, future operations might succeed in a dangerous fashion. Just imagine loops that operate on the same variable.
  4. Confusing output: I mentioned usability in the title. Why subject anyone, including yourself, to a wall of error messages to slog through? Sometimes you need to show multiple errors, but you rarely need to show every single possible error a script can encounter due to one initial failure.

Remember that we use PowerShell to write scripts, not applications. When a program like Word crashes on every error, users rightfully become agitated. When scripts toss out errors and plow on, users rightfully become agitated and we risk damaging something.

How to Finesse PowerShell Error Handling with a Default of Stop

I certainly realize that you might anticipate some errors and want your script to continue if they occur. Due to .Net’s architecture, sometimes you only know that an operation failed because it throws an exception, and you want to handle it instead of halting. Sometimes, your script depend on others’ poorly written scripts that inappropriately throw errors. Sometimes, you want the user to see an error but not stop processing. Setting your default to ‘Stop’ does not prevent you from having fine-grained control over error handling.

Overriding the Error Action for a Specific Cmdlet Invocation

$ErrorActionPreference (also $WarningActionPreference) has multiple possible values. I will only cover the ones that I use; you can read about the others in the official documentation. Note that not every PowerShell version supports every listed option.

  • Continue: System default — writes the error to the error stream but does not halt operation (or in general, trigger a try/catch).
  • SilentlyContinue: Proceed along as though absolutely nothing happened. Nothing goes to the error stream, the script doesn’t stop.
  • Stop: Write the error and stop processing immediately. If within a try block, transfer control to the catch block.

So, when invoking a cmdlet, you can specifically set its error action:

Invoke-Command -ErrorAction Stop #... other parameters ...

This should work for just about any cmdlet that you encounter, even if it is an external advanced function that was not specifically architected to handle it.

Note: I do not fully qualify the error action option when contained within an -ErrorAction parameter. I will talk about that more in the same article on why I do fully qualify the initial setting. Short version: -ErrorAction gives sufficient context on its own. Also, it might not allow full qualification… I have not tried.

Override Example

Consider the following:

# three ways to skin this cat
if(-not Try-FirstWay) {
    if(-not Try-SecondWay) {
        if(-not Try-ThirdWay) {
            throw('Abject failure!')
        }
    }
}

With an error action of Continue, the above will show up to four errors (assuming the Try- cmdlets generate errors). It will also go on to run later script, potentially generating even more errors due to a cascade failure. With an error action of Stop, it will show one error and never even attempt the others, even though you only need one to succeed.

With a change of the default:

$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
# three ways to skin this cat
if(-not Try-FirstWay -ErrorAction SilentlyContinue) {
    if(-not Try-SecondWay -ErrorAction SilentlyContinue) {
        if(-not Try-ThirdWay -ErrorAction SilentlyContinue) {
            throw('Abject failure!')
        }
    }
}

If any one of the three attempts succeeds, the whole script moves along peacefully.

But, if all three fail, the script will display only one message. Also, the entire script will stop here. Anything dependent upon success of at least one of those three methods will not have an opportunity to fail.

I personally prefer Write-Error to throw().

Leveraging Custom Write-Error Messages

If we read the name of the Write-Error cmdlet literally, then we expect that it will write an error. It does that. It also obeys -ErrorAction in a way that not everyone expects. The action undertaken by Write-Error acts like an error.

Try this from within a script (not at an interactive prompt):

$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
Write-Error -Message 'not really an error'
Write-Output -InputObject 'this happens after the error'

If you execute this from within a script, the Write-Output never occurs (and, by extension, nothing else after Write-Error). So, you can use Write-Error with the changed default error action to define custom halt events all in one shot:

$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
$TestCondition = Get-StatusOfThatThing
if($TestCondition -eq 4) {
    Write-Error -Message 'The test failed.'
}
Write-Output -InputObject 'The test succeeded.'

You can also use it to display custom non-blocking errors:

$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
$TestCondition = Get-StatusOfThatThing
if($TestCondition -eq 4) {
    Write-Error -Message 'The test failed, but nobody really liked that test anyway.' -ErrorAction Continue
}
Write-Output -InputObject 'Maybe the test succeeded, maybe it failed. We will go on.'

Leveraging Try/Catch Blocks

Try/Catch blocks neatly solve all sorts of error-handling headaches. If you’ll forgive a bit more pseudo-code:

$result = Try-This
if($result) {
    $result = Try-ThisToo
    if($result) {
        Smile-AtTheOutcome
    }
}

That works well enough. It’s unpleasant to read, though. We can do a lot better, especially with a Stop default error action:

$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
try{
    Try-This
    Try-ThisToo
}
catch{
    Write-Error -Message 'Nothing works, everything is hopeless'
}
Write-Output -InputObject 'Looks like everything will be OK'

This approach gives many gifts:

  1. You get flatter, more legible script.
  2. You can avoid tossing around a generic testing variable by only capturing results that you will use.
  3. You get exactly one error.
  4. PowerShell will not even try the second cmdlet if the first failed, preventing cascade failure.

You want that fourth point for dependent cmdlets:

$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
try{
    $Result = Get-Result
    Manipulate-Result -InputObject $Result
}
catch{
    Write-Error -Message 'Nothing works, everything is hopeless'
}
Write-Output -InputObject 'Looks like everything will be OK'

In the above demonstrations, the catch blocks still cause script termination because I used a Write-Error. Don’t forget the lesson from the preceding section: using -ErrorAction Continue with Write-Error allows the script to go on without stopping. However, you do not need to write an error from a catch block. Typically, we use catch blocks to work around errors so that we do not even need to report them. I did not demonstrate that because most people already use catch blocks that way.

Combine Custom Errors with Try/Catch Blocks

I have one other trick to show you that builds on everything from above. Let’s use this one again, with only one change:

$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
try{
    $Result = Get-Result
    Manipulate-Result -InputObject $Result
}
catch{
    Write-Error -Message 'Nothing works, everything is hopeless' -ErrorAction Continue
    Write-Error -Exception $_.Exception
}

We still allow our script to give its own take on the outcome, but we also show the error that caused all the drama. If you want, you can get clever with it to reduce the total output:

$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
try{
    $Result = Get-Result
    Manipulate-Result -InputObject $Result
}
catch{
    Write-Error -Message ('Nothing works, everything is hopeless, all because of {0}' -f $_.Exception.Message)
}

Or, you can just present the triggering error without the commentary:

catch{
    Write-Error -Exception $_.Exception
}

You might not see the value with such a short example. When you need to execute a longer series of cmdlets with a dependency path, you get more value out of this kind of solution. This allows you to detect exactly which line caused the error without getting a wall of red or risking cascade failure.

I especially like to use custom errors to wrap less comprehensible messages from cmdlets that I didn’t write. For instance, I often need to script System Center Virtual Machine Manager cmdlets, and I swear that team writes all of their error messages using the middle key on their phones. Once I figure out what they really mean, I translate:

$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
try{
    Get-LatestGibberishFromSCVMM
}
catch{
    if($_.Exception.Message -match 'ink window grew plaster distance')
    {
        Write-Error -Message 'The file was not found'
    }
    else
    {
        Write-Error -Message ('not sure what they did this time: {0}' -f $_.Exception.Message)
    }
}

Again, you don’t need to halt in these cases. You can use your catch block to parse the thrown errors and find a non-halting way to deal with them.

General PowerShell Error Guidance

I want you to take two things away from this article:

  • Stop on error messages as your default action. Consider anything else a special case and treat it accordingly.
  • Usability increases when we do not overwhelm the user (including ourselves) with a wall of angry red text.

These work for simple scripts just as well as long ones.

For more complicated scripts, use the error handling tools that PowerShell provides. It does not require much effort. In fact, it can often save you lots of troubleshooting time during the design phase.

Happy scripting!

Advertisements

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 )

Google photo

You are commenting using your Google 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 )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: