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
An exception is generally a terminating error. A thrown exception is either be caught or it
terminates the current execution. By default, a non-terminating error is generated by Write-Error
and it adds an error to the output stream without throwing an exception.
I point this out because Write-Error
and other non-terminating errors do not trigger the
catch
.
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 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.
Try can create terminating errors
Kirk Munro points out that some exceptions are only terminating errors when executed inside a
try/catch
block. Here is the example he gave me that generates a divide by zero runtime exception.
function Start-Something { 1/(1-1) }
Then invoke it like this to see it generate the error and still output the message.
&{ Start-Something; Write-Output "We did it. Send Email" }
But by placing that same code inside a try/catch
, we see something else happen.
try
{
&{ Start-Something; Write-Output "We did it. Send Email" }
}
catch
{
Write-Output "Notify Admin to fix error and send email"
}
We see the error become a terminating error and not output the first message. What I don't like
about this one is that you can have this code in a function and it acts differently if someone is
using a try/catch
.
I have not ran into issues with this myself but it is corner case to be aware of.
$PSCmdlet.ThrowTerminatingError() inside try/catch
One nuance of $PSCmdlet.ThrowTerminatingError()
is that it creates a terminating error within your
Cmdlet but it turns into a non-terminating error after it leaves your Cmdlet. This leaves the burden
on the caller of your function to decide how to handle the error. They can turn it back into a
terminating error by using -ErrorAction Stop
or calling it from within a try{...}catch{...}
.
Public function templates
One last take a way I had with my conversation with Kirk Munro was that he places a
try{...}catch{...}
around every begin
, process
and end
block in all of 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.