Edit

Share via


Everything you wanted to know about exceptions

Error handling is just part of life when it comes to writing code. We can often check and validate conditions for expected behavior. When the unexpected happens, we turn to exception handling. You can easily handle exceptions generated by other people's code or you can generate your own exceptions for others to handle.

Note

The original version of this article appeared on the blog written by @KevinMarquette. The PowerShell team thanks Kevin for sharing this content with us. Please check out his blog at PowerShellExplained.com.

Basic terminology

We need to cover some basic terms before we jump into this one.

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.

Throw and Catch

When an exception happens, we say that an exception is thrown. To handle a thrown exception, you need to catch it. If an exception is thrown and it isn't caught by something, the script stops executing.

The call stack

The call stack is the list of functions that have called each other. When a function is called, it gets added to the stack or the top of the list. When the function exits or returns, it is removed from the stack.

When an exception is thrown, that call stack is checked in order for an exception handler to catch it.

Terminating and non-terminating errors

PowerShell has three categories of errors.

  • A non-terminating error adds an error to the error stream without stopping execution and doesn't trigger catch. By default, Write-Error generates non-terminating errors.
  • A statement-terminating error stops the current statement but allows the script to continue at the next statement. Statement-terminating errors can be generated by engine errors, $PSCmdlet.ThrowTerminatingError(), or .NET method exceptions.
  • A script-terminating error unwinds the entire call stack. Script-terminating errors can be generated by throw, parse errors, or -ErrorAction Stop escalation.

Both statement-terminating and script-terminating errors can be caught by try/catch. For a comprehensive reference, see about_Error_Handling.

Swallowing an exception

This is when you catch an error just to suppress it. Do this with caution because it can make troubleshooting issues very difficult.

Basic command syntax

Here is a quick overview of the basic exception handling syntax used in PowerShell.

Throw

To create our own exception event, we throw an exception with the throw keyword.

function Start-Something
{
    throw "Bad thing happened"
}

This creates a runtime exception that is a script-terminating error. It's handled by a catch in a calling function or exits the script with a message like this.

PS> Start-Something

Bad thing happened
At line:1 char:1
+ throw "Bad thing happened"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (Bad thing happened:String) [], RuntimeException
    + FullyQualifiedErrorId : Bad thing happened

Write-Error -ErrorAction Stop

I mentioned that Write-Error doesn't throw a terminating error by default. If you specify -ErrorAction Stop, Write-Error generates a terminating error that can be handled with a catch.

Write-Error -Message "Houston, we have a problem." -ErrorAction Stop

Thank you to Lee Dailey for reminding about using -ErrorAction Stop this way.

Cmdlet -ErrorAction Stop

If you specify -ErrorAction Stop on any advanced function or cmdlet, it turns all Write-Error statements into terminating errors that stop execution or that can be handled by a catch.

Start-Something -ErrorAction Stop

For more information about the ErrorAction parameter, see about_CommonParameters. For more information about the $ErrorActionPreference variable, see about_Preference_Variables.

Try/Catch

The way exception handling works in PowerShell (and many other languages) is that you first try a section of code and if it throws an error, you can catch it. Here is a quick sample.

try
{
    Start-Something
}
catch
{
    Write-Output "Something threw an exception"
    Write-Output $_
}

try
{
    Start-Something -ErrorAction Stop
}
catch
{
    Write-Output "Something threw an exception or used Write-Error"
    Write-Output $_
}

The catch script only runs if there's a terminating error. If the try executes correctly, then it skips over the catch. You can access the exception information in the catch block using the $_ variable.

Try/Finally

Sometimes you don't need to handle an error but still need some code to execute if an exception happens or not. A finally script does exactly that.

Take a look at this example:

$command = [System.Data.SqlClient.SqlCommand]::new(queryString, connection)
$command.Connection.Open()
$command.ExecuteNonQuery()
$command.Connection.Close()

Anytime you open or connect to a resource, you should close it. If the ExecuteNonQuery() throws an exception, the connection isn't closed. Here is the same code inside a try/finally block.

$command = [System.Data.SqlClient.SqlCommand]::new(queryString, connection)
try
{
    $command.Connection.Open()
    $command.ExecuteNonQuery()
}
finally
{
    $command.Connection.Close()
}

In this example, the connection is closed if there's an error. It also is closed if there's no error. The finally script runs every time.

Because you're not catching the exception, it still gets propagated up the call stack.

Try/Catch/Finally

It's perfectly valid to use catch and finally together. Most of the time you'll use one or the other, but you may find scenarios where you use both.

$PSItem

Now that we got the basics out of the way, we can dig a little deeper.

Inside the catch block, there's an automatic variable ($PSItem or $_) of type ErrorRecord that contains the details about the exception. Here is a quick overview of some of the key properties.

For these examples, I used an invalid path in ReadAllText to generate this exception.

[System.IO.File]::ReadAllText( '\\test\no\filefound.log')

PSItem.ToString()

This gives you the cleanest message to use in logging and general output. ToString() is automatically called if $PSItem is placed inside a string.

catch
{
    Write-Output "Ran into an issue: $($PSItem.ToString())"
}

catch
{
    Write-Output "Ran into an issue: $PSItem"
}

$PSItem.InvocationInfo

This property contains additional information collected by PowerShell about the function or script where the exception was thrown. Here is the InvocationInfo from the sample exception that I created.

PS> $PSItem.InvocationInfo | Format-List *

MyCommand             : Get-Resource
BoundParameters       : {}
UnboundArguments      : {}
ScriptLineNumber      : 5
OffsetInLine          : 5
ScriptName            : C:\blog\throwerror.ps1
Line                  :     Get-Resource
PositionMessage       : At C:\blog\throwerror.ps1:5 char:5
                        +     Get-Resource
                        +     ~~~~~~~~~~~~
PSScriptRoot          : C:\blog
PSCommandPath         : C:\blog\throwerror.ps1
InvocationName        : Get-Resource

The important details here show the ScriptName, the Line of code and the ScriptLineNumber where the invocation started.

$PSItem.ScriptStackTrace

This property shows the order of function calls that got you to the code where the exception was generated.

PS> $PSItem.ScriptStackTrace
at Get-Resource, C:\blog\throwerror.ps1: line 13
at Start-Something, C:\blog\throwerror.ps1: line 5
at <ScriptBlock>, C:\blog\throwerror.ps1: line 18

I'm only making calls to functions in the same script but this would track the calls if multiple scripts were involved.

$PSItem.Exception

This is the actual exception that was thrown.

$PSItem.Exception.Message

This is the general message that describes the exception and is a good starting point when troubleshooting. Most exceptions have a default message but can also be set to something custom when the exception is thrown.

PS> $PSItem.Exception.Message

Exception calling "ReadAllText" with "1" argument(s): "The network path was not found."

This is also the message returned when calling $PSItem.ToString() if there was not one set on the ErrorRecord.

$PSItem.Exception.InnerException

Exceptions can contain inner exceptions. This is often the case when the code you're calling catches an exception and throws a different exception. The original exception is placed inside the new exception.

PS> $PSItem.Exception.InnerExceptionMessage
The network path was not found.

I will revisit this later when I talk about re-throwing exceptions.

$PSItem.Exception.StackTrace

This is the StackTrace for the exception. I showed a ScriptStackTrace above, but this one is for the calls to managed code.

at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean
 useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs,
 String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32
 bufferSize, FileOptions options, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean
 checkHost)
at System.IO.StreamReader..ctor(String path, Encoding encoding, Boolean detectEncodingFromByteOrderMarks,
 Int32 bufferSize, Boolean checkHost)
at System.IO.File.InternalReadAllText(String path, Encoding encoding, Boolean checkHost)
at CallSite.Target(Closure , CallSite , Type , String )

You only get this stack trace when the event is thrown from managed code. I'm calling a .NET Framework function directly so that is all we can see in this example. Generally when you're looking at a stack trace, you're looking for where your code stops and the system calls begin.

Working with exceptions

There is more to exceptions than the basic syntax and exception properties.

Catching typed exceptions

You can be selective with the exceptions that you catch. Exceptions have a type and you can specify the type of exception you want to catch.

try
{
    Start-Something -Path $path
}
catch [System.IO.FileNotFoundException]
{
    Write-Output "Could not find $path"
}
catch [System.IO.IOException]
{
        Write-Output "IO error with the file: $path"
}

The exception type is checked for each catch block until one is found that matches your exception. It's important to realize that exceptions can inherit from other exceptions. In the example above, FileNotFoundException inherits from IOException. So if the IOException was first, then it would get called instead. Only one catch block is invoked even if there are multiple matches.

If we had a System.IO.PathTooLongException, the IOException would match but if we had an InsufficientMemoryException then nothing would catch it and it would propagate up the stack.

Catch multiple types at once

It's possible to catch multiple exception types with the same catch statement.

try
{
    Start-Something -Path $path -ErrorAction Stop
}
catch [System.IO.DirectoryNotFoundException],[System.IO.FileNotFoundException]
{
    Write-Output "The path or file was not found: [$path]"
}
catch [System.IO.IOException]
{
    Write-Output "IO error with the file: [$path]"
}

Thank you Redditor u/Sheppard_Ra for suggesting this addition.

Throwing typed exceptions

You can throw typed exceptions in PowerShell. Instead of calling throw with a string:

throw "Could not find: $path"

Use an exception accelerator like this:

throw [System.IO.FileNotFoundException] "Could not find: $path"

But you have to specify a message when you do it that way.

You can also create a new instance of an exception to be thrown. The message is optional when you do this because the system has default messages for all built-in exceptions.

throw [System.IO.FileNotFoundException]::new()
throw [System.IO.FileNotFoundException]::new("Could not find path: $path")

If you're not using PowerShell 5.0 or higher, you must use the older New-Object approach.

throw (New-Object -TypeName System.IO.FileNotFoundException )
throw (New-Object -TypeName System.IO.FileNotFoundException -ArgumentList "Could not find path: $path")

By using a typed exception, you (or others) can catch the exception by the type as mentioned in the previous section.

Write-Error -Exception

We can add these typed exceptions to Write-Error and we can still catch the errors by exception type. Use Write-Error like in these examples:

# with normal message
Write-Error -Message "Could not find path: $path" -Exception ([System.IO.FileNotFoundException]::new()) -ErrorAction Stop

# With message inside new exception
Write-Error -Exception ([System.IO.FileNotFoundException]::new("Could not find path: $path")) -ErrorAction Stop

# Pre PS 5.0
Write-Error -Exception ([System.IO.FileNotFoundException]"Could not find path: $path") -ErrorAction Stop

Write-Error -Message "Could not find path: $path" -Exception (New-Object -TypeName System.IO.FileNotFoundException) -ErrorAction Stop

Then we can catch it like this:

catch [System.IO.FileNotFoundException]
{
    Write-Log $PSItem.ToString()
}

The big list of .NET exceptions

I compiled a master list with the help of the Reddit r/PowerShell community that contains hundreds of .NET exceptions to complement this post.

I start by searching that list for exceptions that feel like they would be a good fit for my situation. You should try to use exceptions in the base System namespace.

Exceptions are objects

If you start using a lot of typed exceptions, remember that they are objects. Different exceptions have different constructors and properties. If we look at the FileNotFoundException documentation for System.IO.FileNotFoundException, we see that we can pass in a message and a file path.

[System.IO.FileNotFoundException]::new("Could not find file", $path)

And it has a FileName property that exposes that file path.

catch [System.IO.FileNotFoundException]
{
    Write-Output $PSItem.Exception.FileName
}

You should consult the .NET documentation for other constructors and object properties.

Re-throwing an exception

If all you're going to do in your catch block is throw the same exception, then don't catch it. You should only catch an exception that you plan to handle or perform some action when it happens.

There are times where you want to perform an action on an exception but re-throw the exception so something downstream can deal with it. We could write a message or log the problem close to where we discover it but handle the issue further up the stack.

catch
{
    Write-Log $PSItem.ToString()
    throw $PSItem
}

Interestingly enough, we can call throw from within the catch and it re-throws the current exception.

catch
{
    Write-Log $PSItem.ToString()
    throw
}

We want to re-throw the exception to preserve the original execution information like source script and line number. If we throw a new exception at this point, it hides where the exception started.

Re-throwing a new exception

If you catch an exception but you want to throw a different one, then you should nest the original exception inside the new one. This allows someone down the stack to access it as the $PSItem.Exception.InnerException.

catch
{
    throw [System.MissingFieldException]::new('Could not access field',$PSItem.Exception)
}

$PSCmdlet.ThrowTerminatingError()

The one thing that I don't like about using throw for raw exceptions is that the error message points at the throw statement and indicates that line is where the problem is.

Unable to find the specified file.
At line:31 char:9
+         throw [System.IO.FileNotFoundException]::new()
+         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (:) [], FileNotFoundException
    + FullyQualifiedErrorId : Unable to find the specified file.

Having the error message tell me that my script is broken because I called throw on line 31 is a bad message for users of your script to see. It doesn't tell them anything useful.

Dexter Dhami pointed out that I can use ThrowTerminatingError() to correct that.

$PSCmdlet.ThrowTerminatingError(
    [System.Management.Automation.ErrorRecord]::new(
        ([System.IO.FileNotFoundException]"Could not find $Path"),
        'My.ID',
        [System.Management.Automation.ErrorCategory]::OpenError,
        $MyObject
    )
)

If we assume that ThrowTerminatingError() was called inside a function called Get-Resource, then this is the error that we would see.

Get-Resource : Could not find C:\Program Files (x86)\Reference
Assemblies\Microsoft\Framework\.NETPortable\v4.6\System.IO.xml
At line:6 char:5
+     Get-Resource -Path $Path
+     ~~~~~~~~~~~~
    + CategoryInfo          : OpenError: (:) [Get-Resource], FileNotFoundException
    + FullyQualifiedErrorId : My.ID,Get-Resource

Do you see how it points to the Get-Resource function as the source of the problem? That tells the user something useful.

Because $PSItem is an ErrorRecord, we can also use ThrowTerminatingError this way to re-throw.

catch
{
    $PSCmdlet.ThrowTerminatingError($PSItem)
}

This changes the source of the error to the Cmdlet and hide the internals of your function from the users of your Cmdlet.

How try/catch changes error propagation

Inside a try block, PowerShell sets an internal flag that causes all statement-terminating errors to propagate to the catch block. This is by design, not a corner case. The following example demonstrates this behavior.

function Start-Something { 1/(1-1) }

Outside try/catch, a statement-terminating error from a child scope doesn't stop the parent scope. Here is a function that generates a divide by zero runtime exception.

Invoke it like this to see the error reported while the script continues.

&{ Start-Something; Write-Output "We did it. Send Email" }

But placing that same code inside a try/catch, the error propagates to the catch block.

try {
    &{ Start-Something; Write-Output "We did it. Send Email" }
} catch {
    Write-Output "Notify Admin to fix error and send email"
}

The error is caught and the subsequent Write-Output inside the script block doesn't run. This is standard try/catch behavior — all terminating errors within the try block are caught, whether they originate in the current scope or in a child scope.

$PSCmdlet.ThrowTerminatingError() inside try/catch

$PSCmdlet.ThrowTerminatingError() creates a statement-terminating error within the cmdlet. After the error leaves the cmdlet, the caller treats it as a non-terminating error by default. The caller can escalate it back to a terminating error by using -ErrorAction Stop or calling it from within a try/catch block.

Public function templates

One last takeaway I had with my conversation with Kirk Munro was that he places a try/catch block inside every begin, process and end block in all his advanced functions. In those generic catch blocks, he has a single line using $PSCmdlet.ThrowTerminatingError($PSItem) to deal with all exceptions leaving his functions.

function Start-Something
{
    [CmdletBinding()]
    param()

    process
    {
        try {
            ...
        } catch {
            $PSCmdlet.ThrowTerminatingError($PSItem)
        }
    }
}

Because everything is in a try statement within his functions, everything acts consistently. This also gives clean errors to the end user that hides the internal code from the generated error.

Trap

I focused on the try/catch aspect of exceptions. But there's one legacy feature I need to mention before we wrap this up.

A trap is placed in a script or function to catch all exceptions that happen in that scope. When an exception happens, the code in the trap is executed and then the normal code continues. If multiple exceptions happen, then the trap is called over and over.

trap
{
    Write-Log $PSItem.ToString()
}

throw [System.Exception]::new('first')
throw [System.Exception]::new('second')
throw [System.Exception]::new('third')

I personally never adopted this approach but I can see the value in admin or controller scripts that log any and all exceptions, then still continue to execute.

Closing remarks

Adding proper exception handling to your scripts not only make them more stable, but also makes it easier for you to troubleshoot those exceptions.

I spent a lot of time talking throw because it is a core concept when talking about exception handling. PowerShell also gave us Write-Error that handles all the situations where you would use throw. So don't think that you need to be using throw after reading this.

Now that I have taken the time to write about exception handling in this detail, I'm going to switch over to using Write-Error -Stop to generate errors in my code. I'm also going to take Kirk's advice and make ThrowTerminatingError my goto exception handler for every function.