I'm a C# developer and sometimes PowerShell is just driving me mad.
I have the following code:
$script:ErrorActionPreference = 'Stop'
try {
# Some code here
}
catch [Microsoft.PowerShell.Commands.WriteErrorException] {
# Print error messages (without stacktrace)
Write-Host -ForegroundColor Red $_.Exception.Message
exit 1
}
catch [System.Management.Automation.RuntimeException] {
# A thrown string
Write-Host -ForegroundColor Red $_.Exception.Message
Write-Host -ForegroundColor Red $_.ScriptStackTrace
exit 1
}
catch {
# Print proper exception message (including stack trace)
Write-Host -ForegroundColor Red "$($_.Exception.GetType().Name): $($_.Exception.Message)"
Write-Host -ForegroundColor Red $_.ScriptStackTrace
exit 1
}
The idea is basically:
If the exception comes from a call to Write-Error use the first catch block.
If a string is thrown directly, use the second catch block.
For any other exception, use the last catch block.
Now, my problem is with Write-Error and the first catch block:
If I call Write-Error within the try block, the second catch block is executed (even though the first one should be executed).
If I remove the second catch block and then call Write-Error, the correct (first) catch block is used.
Why is that?
I've checked whether WriteErrorException and RuntimeException are inheriting from each other: They don't (both inherit from SystemException but this shouldn't matter).
I've also verified that this behavior is the same in both PowerShell 5.1 and PowerShell Core (6.0).
Write-Error won't throw a terminating error by default, but it will with ErrorActionPreference set to Stop as you've mentioned. However, this changes the exception thrown to ActionPreferenceStopException which does inherit RuntimeException
You can still catch the WriteErrorException without the RuntimeException clause because the inner error record for the ActionPreferenceStopException contains the WriteErrorException
You can see what I mean by running this:
Write-Error 'this is a test' -ErrorAction Stop
$error[0].ErrorRecord.Exception.GetType()
# IsPublic IsSerial Name BaseType
# -------- -------- ---- --------
# True True WriteErrorException System.SystemException
But with the RuntimeException clause, it will get picked up first because RuntimeException is the closest matching exception type.
To workaround this you'd need to either throw a more specific exception or test $_ within the RuntimeException clause. Here's the latter
$script:ErrorActionPreference = 'Stop'
try {
# Some code here
}
catch [Microsoft.PowerShell.Commands.WriteErrorException] {
# Print error messages (without stacktrace)
Write-Host -ForegroundColor Red $_.Exception.Message
exit 1
}
catch [System.Management.Automation.RuntimeException] {
if ($_.Exception -is [Microsoft.PowerShell.Commands.WriteErrorException]) {
# Print error messages (without stacktrace)
Write-Host -ForegroundColor Red $_.Exception.Message
exit 1
}
# A thrown string
Write-Host -ForegroundColor Red $_.Exception.Message
Write-Host -ForegroundColor Red $_.ScriptStackTrace
exit 1
}
catch {
# Print proper exception message (including stack trace)
Write-Host -ForegroundColor Red "$($_.Exception.GetType().Name): $($_.Exception.Message)"
Write-Host -ForegroundColor Red $_.ScriptStackTrace
exit 1
}
You could also add a ActionPreferenceStopException clause and test for $_ there.
Edit: Actually, unless you really want to use Write-Error, you'd be better off just throwing an exception similar to how you would in C#. So instead of Write-Error, use:
throw [System.InvalidOperationException]::new('This is my message')
Related
Here is the code:
Try{
$connection = Test-Connection -BufferSize 32 -Count 1 -ErrorAction Stop -ComputerName "test"
return $connection.StatusCode.ToString()
}
Catch [System.Net.NetworkInformation.PingException]{
return "Ping Exception"
}
Catch [Exception]{
return "Unexpected exception"
}
Now, let's consider the case where the -ComputerName would not be found, this would return me a System.Net.NetworkInformation.PingException. But, in the above code the output would be Unexpected exception.
Refering to this answer, I should use System.Management.Automation.ActionPreferenceStopException to catch it.
Now my question is, how can I catch the last inner exception when using the -ErrorAction Stop flag. Should I just throw a PingException ? It doesn't seems to be a good idea since I can't be sure a PinException really is the cause of the ErrorAction trigger.
Turns out that when using -ErrorAction Stop flag, non-terminating errors are wrapped and thrown as type System.Management.Automation.ActionPreferenceStopException. Therefore a solution would be to walk through the exception tree like so,
Try{
$connection = Test-Connection -BufferSize 32 -Count 1 -ErrorAction Stop -ComputerName "test"
return $connection.StatusCode.ToString()
}
Catch [System.Management.Automation.ActionPreferenceStopException]{
$exception = $_.Exception
#Walk through Exception tree
while ($exception.InnerException) {
$exception = $exception.InnerException
}
#Return only the last inner exception
return $exception.Message
}
Catch [Exception]{
return "Unexpected exception"
}
EDIT
Note that my code is returning the last inner exception Message as a string. The same logic can be used to find other information if needed.
I am trying to handle an ActiveDirectoryObjectNotFoundException exception in PowerShell when using the Forest.GetForest method.
https://msdn.microsoft.com/en-us/library/system.directoryservices.activedirectory.forest.getforest(v=vs.110).aspx
# Clear screen
Clear
# Change below as per your requirements
$context='forest'
$name='My.Lab.Local'
$username="fake\Administrator"
$password="FakePassword"
Write-Host -Object "Connecting $context... -> $name " -BackgroundColor Yellow -ForegroundColor Blue
try
{
$DC = new-object System.DirectoryServices.ActiveDirectory.DirectoryContext($context,$name,$username,$password)
Write-Host -Object "Successfully connected to $context using discovery account $username." -BackgroundColor Green -ForegroundColor Blue
Write-Host -Object "Retrieving details of the forest..." -BackgroundColor Yellow -ForegroundColor Blue
$Forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($DC)
$Forest.Name
Write-Host -Object "Successfully retrived your forest..." -BackgroundColor Green -ForegroundColor Blue
}
catch [System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException]
{
Write-Host "ActiveDirectoryObjectNotFoundException exception"
}
catch [System.Security.Authentication.AuthenticationException]
{
Write-Host "AuthenticationException exception ( Catch Block )"
}
finally
{
Write-Host "cleaning up ...( Finally Block )"
}
The Output
Connecting forest... -> Web.Metacash.Com
Successfully connected to forest using discovery account fake\Administrator.
Retrieving details of the forest...
AuthenticationException exception ( Catch Block )
cleaning up ...( Finally Block )
How to do I get the original failing message instead of giving my own message, like using $_.message or something?
$_ should have an ErrorRecord in the catch block. The exception should be in there. For example, use $_.Exception.Message to get its message. Of course the error record has more info about the error. $_.InvocationInfo.ScriptLineNumber would have the line number where the error occurred.
Consider this simple code:
Read-Host $path
try {
Get-ChildItem $Path -ErrorAction Continue
}
Catch {
Write-Error "Path does not exist: $path" -ErrorAction Stop
Throw
}
Write-Output "Testing"
Why is 'Testing' printed to the shell if an invalid path is specified?
The script does not stop in the catch block. What am I doing wrong?
In your Try Catch block, you need to set Get-ChildItem -ErrorAction Stop
so the exception is caught in the Catch block.
With continue, you are instructing the command to not produce a terminating error when an actual error occurs.
Edit:
Also, your throw statement is useless there and you do not need to specify an error-action for Write-Error.
Here's the modified code.
$path = Read-Host
try {
Get-ChildItem $Path -ErrorAction stop
}
Catch {
Write-Error "Path does not exist: $path"
}
Additional note
You could apply this default behavior (if that is what you want) to the entire script by setting the default action to stop using :
$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
I think this is what you need:
$path = Read-Host 'Enter a path'
try {
Get-ChildItem $Path -ErrorAction Stop
}
Catch {
Throw "Path does not exist: $path"
}
Write-Output "Testing"
Per Sage's answer, you need to change to -ErrorAction Stop in the Try block. This forces the Get-ChildItem cmdlet to throw a terminating error, which then triggers the Catch block. By default (and with the Continue ErrorAction option) it would have thrown a non-terminating error which are not caught by a try..catch.
If you then want your code to stop in the Catch block, use Throw with the message you want to return. This will produce a terminating error and stop the script (Write-Error -ErrorAction Stop will also achieve a terminating error, it's just a more complicated method. Typically you should use Write-Error when you want to return non-terminating error messages).
I'm unable to catch the DriveNotFoundException being generated by Get-PSDrive in the following example:
try {
# Assumes no Q:\ drive connected.
$foo = Get-PSDrive -name 'Q' -ErrorAction Stop
}
catch [System.Management.Automation.DriveNotFoundException] {
Write-Output "Drive not found."
}
catch {
Write-Output "Something else went wrong."
}
This should print the following:
PS C:\temp> .\foo.ps1
Drive not found.
PS C:\temp>
Instead, I get:
PS C:\temp> .\foo.ps1
Something else went wrong.
PS C:\temp>
I'm using Powershell 2.0, if that's relevant.
The issue is because the -ErrorAction Stop is changing the Exception type that the try/catch block is seeing.
You can prove it by catching the ActionPreferenceStopException type. So let's run some troubleshooting code to see what is going on:
try {
# Assumes no Q:\ drive connected.
$foo = Get-PSDrive -name 'Q' -ErrorAction Stop
}
catch [System.Management.Automation.DriveNotFoundException] {
Write-Output "Drive not found."
}
catch [System.Management.Automation.ActionPreferenceStopException] {
Write-Output "Stop Exception."
write-host "Caught an exception:" -ForegroundColor Red
write-host "Exception Type: $($_.Exception.GetType().FullName)" -ForegroundColor Red
write-host "Exception Message: $($_.Exception.Message)" -ForegroundColor Red
}
catch
{
write-host "Caught an exception:" -ForegroundColor Red
write-host "Exception Type: $($_.Exception.GetType().FullName)" -ForegroundColor Red
write-host "Exception Message: $($_.Exception.Message)" -ForegroundColor Red
}
This returns the following output:
Stop Exception.
Caught an exception:
Exception Type: System.Management.Automation.DriveNotFoundException
Exception Message: Cannot find drive. A drive with the name 'Q' does not exist.
So you see that the try/catch caught the [System.Management.Automation.ActionPreferenceStopException] exception, Even Though the Exception type is [System.Management.Automation.DriveNotFoundException] inside the catch block.
So, we can handle it with a slightly modified version of #haliphax's solution, which is to check the error type inside the ActionPreferenceStopException catch block:
try {
# Assumes no Q:\ drive connected.
$foo = Get-PSDrive -name 'Q' -ErrorAction Stop
}
catch [System.Management.Automation.ActionPreferenceStopException] {
if ($Error[0].Exception.GetType().Name -eq 'DriveNotFoundException') {
Write-Output "Drive not found."
}
else {
Write-Output "Something else went wrong."
}
}
catch {
Write-Output "Something else went wrong."
}
To complement HAL9256's great answer:
Note: The following is in part speculative. Do let me know if I'm wrong.
The behavior observed is a presumably a bug in PowerShell v1 and v2 , where the internal exception of type [System.Management.Automation.ActionPreferenceStopException] accidentally masks the original exception in the matching logic of typed catch blocks in a try / catch statement.
The reason I suspect that [System.Management.Automation.ActionPreferenceStopException] is purely an internal exception that should never have been exposed, is that both $Error[0].Exception and its alias inside a catch block, $_.Exception, reflect the original exception, even in PowerShell v1 and v2 - the $Errors collection contains no trace of [System.Management.Automation.ActionPreferenceStopException].
While the bug is fixed in v3+, v3+ still, but now also matches [System.Management.Automation.ActionPreferenceStopException] in typed catch handlers, presumably so as not to break backward compatibility.
There is little benefit in catching [System.Management.Automation.ActionPreferenceStopException], given its generic nature (it just tells you that a cmdlet experienced a non-terminating error).
The only conceivable reason for catching it is if you wanted to know whether the exception at hand was natively terminating or only treated as terminating due to -ErrorAction Stop or $ErrorActionPreference = 'Stop'.
Thus, for code that must also on run v2-, I'd solve the problem as follows:
try {
# Assumes no Q:\ drive connected.
$foo = Get-PSDrive -name 'Q' -ErrorAction Stop
}
catch { # Use a generic handler to work around the bug in v1 an v2.
# $_ is the [System.Management.Automation.ErrorRecord] instance representing
# the PowerShell error at hand (same as $Error[0]).
# $_.Exception contains the exception that triggered the error,
# and can be compared to specific exception types with -is.
if ($_.Exception -is [System.Management.Automation.DriveNotFoundException]) {
"Drive not found."
} else {
"Something else went wrong."
}
}
I realize that this is due to some strange behavior (according to the above comments about later versions of PowerShell), but this does manage to handle the specific error:
try {
$foo = Get-PSDrive -name 'Q' -ErrorAction Stop
}
catch {
if ($Error[0].Exception.GetType().Name -eq 'DriveNotFoundException') {
Write-Output 'No such drive.'
}
else {
Write-Output 'Something else went wrong.'
}
}
This is just a brief post of the final version of the code I used, based on the explanation provided by #HAL9256 in his answer:
try {
# Assumes no Q:\ drive connected.
$foo = Get-PSDrive -name 'Q' -ErrorAction Stop
}
# Catch exceptions thrown by both v2.0 and by later versions.
catch [System.Management.Automation.ActionPreferenceStopException], `
[System.Management.Automation.DriveNotFoundException] {
Write-Output "Drive not found."
}
catch {
Write-Output "Something else went wrong."
}
This has been tested on both PowerShell 2.0 & 4.0 and works on both. I suppose there's a minor risk that some other exception will occur in the Get-PSDrive statement in a PowerShell 2.0 environment, triggering the catch block, but in my use case, it's an acceptable risk and will trigger a different exception later in the script.
I am new to powershell, and I am trying to add error handling via try/catch statements, but they don't seem to actually be catching the error. This is powershell v2 CP3.
$objComputer = $objResult.Properties;
$strComputerName = $objComputer.name
write-host "Checking machine: " $strComputerName
try
{
$colItems = get-wmiobject -class "Win32_PhysicalMemory" -namespace "root\CIMV2" -computername $strComputerName -Credential $credentials
foreach ($objItem in $colItems)
{
write-host "Bank Label: " $objItem.BankLabel
write-host "Capacity: " ($objItem.Capacity / 1024 / 1024)
write-host "Caption: " $objItem.Caption
write-host "Creation Class Name: " $objItem.CreationClassName
write-host
}
}
Catch
{
write-host "Failed to get data from machine (Error:" $_.Exception.Message ")"
write-host
}
finally
{ }
When it fails to contact a specific machine, I get this in console, and not my clean catch message:
Get-WmiObject : The RPC server is
unavailable. (Exception from HRESULT:
0x800706BA) At Z:\7.0 Intern
Programvare\Powershell\Get memory of
all computers in AD.ps1:25 char:34
+ $colItems = get-wmiobject <<<< -class "Win32_PhysicalMemory"
-namespace "root\CIMV2" -computername $strComputerName -Credential
$credentials
+ CategoryInfo : InvalidOperation: (:) [Get-WmiObject],
COMException
+ FullyQualifiedErrorId : GetWMICOMException,Microsoft.PowerShell.Commands.GetWmiObjectCommand
I was able to duplicate your result when trying to run a remote WMI query. The exception thrown is not caught by the Try/Catch, nor will a Trap catch it, since it is not a "terminating error". In PowerShell, there are terminating errors and non-terminating errors . It appears that Try/Catch/Finally and Trap only works with terminating errors.
It is logged to the $error automatic variable and you can test for these type of non-terminating errors by looking at the $? automatic variable, which will let you know if the last operation succeeded ($true) or failed ($false).
From the appearance of the error generated, it appears that the error is returned and not wrapped in a catchable exception. Below is a trace of the error generated.
PS C:\scripts\PowerShell> Trace-Command -Name errorrecord -Expression {Get-WmiObject win32_bios -ComputerName HostThatIsNotThere} -PSHost
DEBUG: InternalCommand Information: 0 : Constructor Enter Ctor
Microsoft.PowerShell.Commands.GetWmiObjectCommand: 25857563
DEBUG: InternalCommand Information: 0 : Constructor Leave Ctor
Microsoft.PowerShell.Commands.GetWmiObjectCommand: 25857563
DEBUG: ErrorRecord Information: 0 : Constructor Enter Ctor
System.Management.Automation.ErrorRecord: 19621801 exception =
System.Runtime.InteropServices.COMException (0x800706BA): The RPC
server is unavailable. (Exception from HRESULT: 0x800706BA)
at
System.Runtime.InteropServices.Marshal.ThrowExceptionForHRInternal(Int32 errorCode, IntPtr errorInfo)
at System.Management.ManagementScope.InitializeGuts(Object o)
at System.Management.ManagementScope.Initialize()
at System.Management.ManagementObjectSearcher.Initialize()
at System.Management.ManagementObjectSearcher.Get()
at Microsoft.PowerShell.Commands.GetWmiObjectCommand.BeginProcessing()
errorId = GetWMICOMException errorCategory = InvalidOperation
targetObject =
DEBUG: ErrorRecord Information: 0 : Constructor Leave Ctor
System.Management.Automation.ErrorRecord: 19621801
A work around for your code could be:
try
{
$colItems = get-wmiobject -class "Win32_PhysicalMemory" -namespace "root\CIMV2" -computername $strComputerName -Credential $credentials
if ($?)
{
foreach ($objItem in $colItems)
{
write-host "Bank Label: " $objItem.BankLabel
write-host "Capacity: " ($objItem.Capacity / 1024 / 1024)
write-host "Caption: " $objItem.Caption
write-host "Creation Class Name: " $objItem.CreationClassName
write-host
}
}
else
{
throw $error[0].Exception
}
If you want try/catch to work for all errors (not just the terminating errors) you can manually make all errors terminating by setting the ErrorActionPreference.
try {
$ErrorActionPreference = "Stop"; #Make all errors terminating
get-item filethatdoesntexist; # normally non-terminating
write-host "You won't hit me";
} catch{
Write-Host "Caught the exception";
Write-Host $Error[0].Exception;
}finally{
$ErrorActionPreference = "Continue"; #Reset the error action pref to default
}
Alternatively... you can make your own try/catch function that accepts scriptblocks so that your try/catch calls are not as kludge. I have mine return true/false just in case I need to check if there was an error... but it doesn't have to. Also, exception logging is optional, and can be taken care of in the catch, but I found myself always calling the logging function in the catch block, so I added it to the try/catch function.
function log([System.String] $text){write-host $text;}
function logException{
log "Logging current exception.";
log $Error[0].Exception;
}
function mytrycatch ([System.Management.Automation.ScriptBlock] $try,
[System.Management.Automation.ScriptBlock] $catch,
[System.Management.Automation.ScriptBlock] $finally = $({})){
# Make all errors terminating exceptions.
$ErrorActionPreference = "Stop";
# Set the trap
trap [System.Exception]{
# Log the exception.
logException;
# Execute the catch statement
& $catch;
# Execute the finally statement
& $finally
# There was an exception, return false
return $false;
}
# Execute the scriptblock
& $try;
# Execute the finally statement
& $finally
# The following statement was hit.. so there were no errors with the scriptblock
return $true;
}
#execute your own try catch
mytrycatch {
gi filethatdoesnotexist; #normally non-terminating
write-host "You won't hit me."
} {
Write-Host "Caught the exception";
}
It is also possible to set the error action preference on individual cmdlets, not just for the whole script. This is done using the parameter ErrorAction (alisa EA) which is available on all cmdlets.
Example
try
{
Write-Host $ErrorActionPreference; #Check setting for ErrorAction - the default is normally Continue
get-item filethatdoesntexist; # Normally generates non-terminating exception so not caught
write-host "You will hit me as exception from line above is non-terminating";
get-item filethatdoesntexist -ErrorAction Stop; #Now ErrorAction parameter with value Stop causes exception to be caught
write-host "you won't reach me as exception is now caught";
}
catch
{
Write-Host "Caught the exception";
Write-Host $Error[0].Exception;
}
This is my solution. When Set-Location fails it throws a non-terminating error which is not seen by the catch block. Adding -ErrorAction Stop is the easiest way around this.
try {
Set-Location "$YourPath" -ErrorAction Stop;
} catch {
Write-Host "Exception has been caught";
}
Adding "-EA Stop" solved this for me.
Edit: As stated in the comments, the following solution applies to PowerShell V1 only.
See this blog post on "Technical Adventures of Adam Weigert" for details on how to implement this.
Example usage (copy/paste from Adam Weigert's blog):
Try {
echo " ::Do some work..."
echo " ::Try divide by zero: $(0/0)"
} -Catch {
echo " ::Cannot handle the error (will rethrow): $_"
#throw $_
} -Finally {
echo " ::Cleanup resources..."
}
Otherwise you'll have to use exception trapping.
In my case, it was because I was only catching specific types of exceptions:
try
{
get-item -Force -LiteralPath $Path -ErrorAction Stop
#if file exists
if ($Path -like '\\*') {$fileType = 'n'} #Network
elseif ($Path -like '?:\*') {$fileType = 'l'} #Local
else {$fileType = 'u'} #Unknown File Type
}
catch [System.UnauthorizedAccessException] {$fileType = 'i'} #Inaccessible
catch [System.Management.Automation.ItemNotFoundException]{$fileType = 'x'} #Doesn't Exist
Added these to handle additional the exception causing the terminating error, as well as unexpected exceptions
catch [System.Management.Automation.DriveNotFoundException]{$fileType = 'x'} #Doesn't Exist
catch {$fileType='u'} #Unknown