Azure and DevOops

How to work with errors in PowerShell

Β· Christian Piet

In my career, I’ve seen many different approaches to errors, all meant to address them. However, none of these approaches provided a complete solution. Either everything is dumped without a specific error, the script/function continues while it should error, or the wrong keywords are used, like break. Code with proper error handling is more reliable, easier to maintain, and makes it easier to understand what is happening. In this blog, I will share what I have learned on how to create errors, how to handle errors, and what not to do.

Errors ❌

Errors are unexpected events that occur when executing code. For example, errors can happen when we fail to process objects. Luckily, you can anticipate errors. In PowerShell, everything is an object, so errors are objects, too. Errors contain the error message and a property called the exception.

I borrowed the definition of an exception from a Microsoft Learn page, which adopted an article from Kevin Marquette his series “Everything you want to know about *”. In his post there is a pretty definition of an exception:

An Exception is like an event that is created when normal error handling can’t deal with the issue. Trying to divide a number by zero or running out of memory are examples of something that creates an exception. Sometimes the author of the code you’re using creates exceptions for certain issues when they happen.

Exceptions are objects in PowerShell, too. In some cases they even have specific types. They contain which line PowerShell failed where, with which argument, and the whole call stack (the ‘stack’ of commands called to get to that point). So, exception objects are precious because they can show you information on what went wrong.

Error types πŸ†Ž

In PowerShell, there are two types of errors: terminating and non-terminating errors. A non-terminating error fails to process a request but doesn’t stop processing altogether. For example, if you copy the contents of a directory into another directory and it fails to copy one item, should it stop copying all other files? With a non-terminating error, it would continue, but a terminating error will stop all further execution.

When a terminating error occurs while executing a function, it stops the code’s execution. Depending on the specified ErrorAction or ErrorActionPreference setting, PowerShell will stop running the rest of the script, function, or runspace. By changing the ErrorAction or ErrorActionPreference to Stop, you can change the behavior of errors from non-terminating to terminating.

The fastest way to understand what I mean is by considering the following code:

 1function Write-Filler {
 2 [CmdletBinding()]
 3    param ([Parameter(ValueFromPipeline)]$Imnotused)
 4    Write-Host "I am filler"
 5}
 6function Write-NonTerminatingError {
 7 [CmdletBinding()]
 8    param ()
 9    Write-Error -Message "This is a non-terminating error"
10    Write-Host "This is inside of the function after the non-terminating error"
11}
12function Write-TerminatingError {
13 [CmdletBinding()]
14    param ()
15    Write-Error -Message "This is a terminating error" -ErrorAction Stop
16    Write-Host "This is inside of the function after the terminating error"
17}
18
19Write-Host "Start"
20
21Write-NonTerminatingError | Write-Filler
22Write-TerminatingError | Write-Filler
23
24Write-Host "End"

In the code above, I wrote an empty filler function to show that pipeline processing continues. I also wrapped all the error messages in functions for the same argument.

When you run this code, you can see that the code inside of the function Write-NonTerminatingError is still executed even after the error because it’s a non-terminating error. PowerShell also executes the command that happens after the | pipe character. When code executing reaches Write-TerminatingError, the code runs up to Write-Error-statement inside the function, with the ErrorAction value of Stop specified. This ErrorAction Statement stops all further code execution when receiving an error. The pattern to write a terminating error this way is uncommon, but using the throw statement combined with try/catch is more common. I’ll continue on that later.

Statement terminating error πŸ’¬

There is a way to throw a statement terminating error, meaning it won’t continue processing that specific line, but it will continue with the rest of the script. You can create this type of statement terminating error within your function by typing $PSCmdlet.ThrowTerminatingError(). This method on the $PSCmdlet-object, which is available in cmdlets/advanced functions, has a specific argument/overload to provide an ErrorRecord-object. The neat thing about this way of creating a statement terminating error is that it terminates the statement but not all other processing after the statement.

Consider the following code addition to our previous codeblock, all of the arguments to ThrowTerminatingError aren’t relevant, but the $PSCmdlet.ThrowTerminatingError and Write-Host are:

 1function Write-StatementTerminatingError {
 2 [CmdletBinding()]
 3    param ()
 4    $PSCmdlet.ThrowTerminatingError(
 5 [System.Management.Automation.ErrorRecord]::new(
 6 [Exception]::new("This is a statement terminating error"),
 7            'ErrorID',
 8 [System.Management.Automation.ErrorCategory]::OperationStopped,
 9            'TargetObject')
10 )
11    Write-Host "This is inside of the function after the statement terminating error"
12}
13"Start"
14Write-NonTerminatingError | Write-Filler
15Write-StatementTerminatingError | Write-Filler
16Write-TerminatingError | Write-Filler
17"End"

When executing this code, it reacted like a non-terminating error. You couldn’t read the message after the statement-terminating error in the output, nor did it execute/output Write-Filler its statement. It just stops the code in that function and pipeline processing. However, execution does continue on the rest of the script since it ran Write-TerminatingError.

Using statement-terminating errors can be very convenient if you create functions for a script or module, which should stop the processing of the pipeline. By using statement-terminating errors you leave the decision to stop processing altogether up to the user. Since statement-terminating errors can only be used in advanced functions (functions with [CmdletBinding()] and/or param() in them), you can always specify whether you want the script to stop running by setting up ErrorAction on that line or changing the ErrorActionPreference.

1Write-StatementTerminatingError -ErrorAction Stop | Write-Filler

I use statement terminating errors a lot during module development. I only use ’true’ terminating errors when continuing is impossible or potentially destructive. So if you can’t get a token or refresh it, or when you can’t connect even after retries, it’s futile to continue. I haven’t found a use case for implementing non-terminating errors.

throw ⚾️

Now, you might’ve wondered, but what about throw? Well, throw causes a terminating error. In the previous section I referenced the following codeblock:

1function Write-TerminatingError {
2 [CmdletBinding()]
3    param ()
4    Write-Error -Message "This is a terminating error" -ErrorAction Stop
5    Write-Host "This is inside of the function after the terminating error"
6}

The -ErrorAction Stop argument to a function creating an error will stop execution. Since Write-Error creates an error upon execution (duh) and the ErrorAction is set to Stop, PowerShell terminates the processing. It can be inconvenient to spot the ErrorAction if it’s a long error message, especially when an alias is used like β€”ea (please just auto-resolve these in VSCode using ALT+SHIFT+E; thank you for teaching me this, Barbara Forbes).

That is where throw comes in; if you read throw in a script, you know it’s a full stop if it hits that code. If throw is used within a script outside of a try/catch block it’ll generate a ScriptHalted error and stop all the things. You’ll see the following: Exception: ScriptHalted, which is not very helpful. Luckily, throw will accept expressions like error messages similar to Write-Error but can also accept objects as arguments.

In the previously mentioned script, we can replace the terminating error of Write-Error -ErrorAction Stop with the more commonly used throw. throw will behave similarly to what we had earlier:

 1function Write-TerminatingError {
 2 [CmdletBinding()]
 3    param ()
 4    throw "This is a terminating error"
 5    Write-Host "This is inside of the function after the terminating error"
 6}
 7"Start"
 8Write-NonTerminatingError | Write-Filler
 9Write-StatementTerminatingError | Write-Filler
10Write-TerminatingError | Write-Filler
11"End"

throw is affected by ErrorAction/ErrorActionPreference. So be cautious when using Ignore and SilentlyContinue as this can create unexpected behavior.

 1function Do-Stuff {
 2 [CmdletBinding()]
 3    param ()
 4    throw
 5    Write-Host "I come after the big error"
 6}
 7
 8"Start"
 9Do-Stuff -ErrorAction Ignore
10"I am also executed, weird right?"
11"End"

While terminating errors can occur within functions or scripts, it’s up to us to catch those. In some cases, if something fails, you might want to retry it, create a ticket, log something specific to a log, or close a connection, but that’s all dependent on what type of error happened, right?

try/catch 🎣

try/catch is a functionality you can use to prepare and enact on terminating errors. The try part is a script block in which you assume (terminating) errors could happen. If terminating errors occur within the script block, processing of the code within the try block is terminated, and the error record, with its exception property, is passed to the catch script block as the pipeline item ($_ or $PSItem).

In the following code, we’d want to know more about a specific directory and its contents only when something goes wrong. The catch block is an excellent way to do so:

 1try {
 2    Write-Host "Start try"
 3    Get-Item "LegoMilleniumFalcon" -ErrorAction Stop
 4    Write-Host "End try"
 5} catch {
 6    Write-Host "Start catch"
 7    Write-Host "ErrorItemName: $($_.Exception.ItemName)"
 8    Get-ChildItem ($_.Exception.ItemName | Split-Path)
 9    throw
10}

I tried to find a LegoMilleniumFalcon item, but PowerShell couldn’t find it. However, Get-Item doesn’t throw a non-terminating error when it cannot find an item. That is convenient and nice of the devs, but if we want to stop all processing and find out why, we must access the catch block. To do so we need to make our script or Get-Item’s error behavior to be terminating. By setting the -ErrorAction Stop, we change the non-terminating error to terminating so we can enter the catch block for more information.

If you executed this code and you read Write-Host “End try” you either have a LegoMilleniumFalcon file or directory in your machine, or you changed the ErrorAction (oh you). But in all other cases, you can see that PowerShell failed to catch the item, the catch was started (just for demo purposes), which item failed to process, and what the contents of the directory that failed were. If the directory is non-existent, Get-ChildItem will throw a non-terminating error.

The sharpest among you saw that we have throw in the catch block creating another terminating error, so our script stops after that statement. Also, you might’ve noticed that we didn’t provide another argument to throw. If throw has no arguments within a catch block, it’ll rethrow the error record it was called with. So the error record in $_/$PSItem, available in the catch block, will be rethrown. Rethrowing can be confusing; submitting a comment might be nice.

You can also catch specific exceptions, the error-object contains the exception-object, but that’s also a specific type. If you’re debugging and want specific handling for this type of exception, you can get the type with:

1$_.Exception.gettype().Fullname

Doing so allows us to do the following:

 1try {
 2    Write-Host "Start try"
 3    Get-Item "LegoMilleniumFalcon" -ErrorAction Stop
 4    Write-Host "End try"
 5} catch [System.Management.Automation.ItemNotFoundException]{
 6    Write-Host "ErrorItemName:"
 7    Write-Host $_.Exception.ItemName
 8    Get-ChildItem 'blablbla'
 9    throw
10} catch {
11    Write-Host "Start generic catch"
12    throw
13}

Catching specific exception types is very handy if the implementation of your function implements specific exception types.

Try/catch as a mechanism is very handy because you can enclose each section of your code within a try statement. Sometimes you don’t want your script to stop, but just to throw a statement terminating error. In that case you can apply what you learned earlier and use a statement terminating error that reuses the error record of the failed action:

 1function Get-NotSoImportantThing {
 2 [CmdletBinding()]
 3    param ()
 4    try {
 5        Write-Host "Start try"
 6        Get-Item "Thisisnotthefileyouarelookingfor" -ErrorAction Stop
 7        Write-Host "End try"
 8 } catch {
 9        $PSCmdlet.ThrowTerminatingError($_)
10 }
11}
12
13"Start"
14Get-NotsoImportantThing | Write-Filler
15"Very Important code"
16"End"

Lastly, I also use throw in scripts in case I enter an unhappy code path in my script that doesn’t generate an error but is something we don’t want to continue on. But as a reminder, when developing functions/cmdlets you want to leave that decision up to the user and a statement terminating error is almost always the way to go.

For example, I have a script that processes a JSON file and misses a specific property. If I didn’t check the JSON manually beforehand, the processing of the JSON will potentially error out later on, but I don’t want to exit thinking it was completed successfully. In that case, you can choose to throw a terminating error with an error message or with a fully custom-made error record.

 1$jsonConverted = @'
 2{
 3 "aryaStark": {
 4 "knows": "needle"
 5 },
 6 "johnSnow": {
 7 "knows": null
 8 }
 9}
10'@ | ConvertFrom-Json -AsHashtable
11
12foreach ($key in $jsonConverted.Keys) {
13    if ([String]::IsNullOrWhiteSpace($jsonConverted[$key].knows)) {
14        throw "$key knows nothing"
15 }
16    else {
17        Write-Host "$key knows $($jsonConverted[$key].knows)"
18 }
19}

Custom errors

Sometimes, your scripts require you to create a typed exception object to do specific catches. You can do so with the code I referenced earlier:

 1function Write-StatementTerminatingError {
 2 [CmdletBinding()]
 3    param ()
 4    $PSCmdlet.ThrowTerminatingError(
 5 [System.Management.Automation.ErrorRecord]::new(
 6 [Exception]::new("This is a statement terminating error"),
 7            'ErrorID',
 8 [System.Management.Automation.ErrorCategory]::OperationStopped,
 9            'TargetObject')
10 )
11    Write-Host "This is inside of the function after the statement terminating error"
12}
13"Start"
14Write-StatementTerminatingError | Write-Filler
15"End"

The type of exception created with this error ErrorRecord/Error-object, is System.Exception. Keep in mind that types aren’t exported in PowerShell script modules. To do so, you must create a binary module in C# to create a custom type you can catch up on in a catch block. Therefore if you’re not proficient in C#, you could use a hacky pattern:

 1function Write-StatementTerminatingError {
 2 [CmdletBinding()]
 3    param ()
 4    Write-Host "Start of your function code"
 5 $ThisIsBrokenBeyondFixing = $true
 6    try {
 7        if ($ThisIsBrokenBeyondFixing) {
 8            throw "aah this run of Write-StatementTerminatingError is broken beyond automatic repair"
 9 }
10 } catch {
11        $PSCmdlet.ThrowTerminatingError($_)
12 }
13    Write-Host "This is inside of the function after the statement terminating error"
14}
15
16"Start"
17try {
18    Write-StatementTerminatingError | Write-Filler
19} catch {
20    if ($_.Exception.Message -eq "aah this run of Write-StatementTerminatingError is broken beyond automatic repair") {
21        Write-host "Inside of the specific catchblock"
22        throw
23 }
24}
25"End"

By throwing inside the function’s try block, you create an error record for an exception containing a specific message or object. The function’s catch block can then be matched with if-logic to create a similar behavior. Alternatively, you can learn C# and create your own types of exceptions. Do note that this is hacky; I’d discuss this with my team if you want this type of hackyness in your codebase.

Conclusion

Handling errors effectively in PowerShell is crucial for creating robust and reliable scripts. Understanding the difference between terminating and non-terminating errors allows you to control the flow of your scripts more precisely. Using Write-Error with -ErrorAction, $PSCmdlet.ThrowTerminatingError(), and throw statements, you can manage how errors are handled and ensure that your scripts behave as expected in various error scenarios.

The try/catch mechanism provides a structured way to handle terminating errors. It allows you to perform specific actions when errors occur and maintain control over your script’s execution. By catching specific exceptions, you can tailor your error handling to different types of errors, making your scripts more resilient and easier to debug.

Custom exceptions and advanced error-handling techniques, such as creating custom exception objects or using statement-terminating errors, enhance your ability to manage errors in complex scripts and modules. But they might require you to learn C# or do hacky things. Either way discuss with your team what suits your codebase best and how you handle these things. Or even better, tell us at the (PowerShell Discord) or your PowerShell user group.

Applying these error techniques allows you to create PowerShell scripts that handle errors gracefully, provide meaningful feedback, and continue processing when appropriate, leading to more reliable and maintainable code.

I couldn’t have written this post without the guidance of the true pro’s on the PowerShell discord through the years. Thank you all

#error #exception #terminating error #non-terminating error #$pscmdlet.throwterminatingerror() #throw #try/catch