Is there a convenient way to catch types of exceptions and inner exceptions for try-catch purposes?
Example code:
$a = 5
$b = Read-Host "Enter number"
$c = $a / $b #error if $b -eq 0
$d = get-content C:\I\Do\Not\Exist
Row #3 will generate a runtime error with an inner exception (EDIT: fixed this command $Error[1].Exception.InnerException.GetType()), row #4 will generate a "standard"(?) type of exception ($Error[0].Exception.GetType()).
Is it possible to get the desired result from both of these with the same line of code?
Ad1: error from row 3
At -path-:3 char:1
+ $c = $a / $b #error if $b -eq 0
+ ~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], RuntimeException
+ FullyQualifiedErrorId : RuntimeException
Ad2: error from row 4
get-content : Cannot find path 'C:\I\Do\Not\Exist' because it does not exist.
At -path-:4 char:6
+ $d = get-content C:\I\Do\Not\Exist
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\I\Do\Not\Exist:String)
[Get-Content], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetContentCommand
Edit: To make it clear, I want the result to return DivideByZeroException and ItemNotFoundException in some way
First of all, you can explicitly catch specific exception types:
$ErrorActionPreference = "Stop"
try {
1 / 0
}
catch [System.DivideByZeroException] {
$_.Exception.GetType().Name
}
try {
Get-Item "c:\does-not-exist"
}
catch [System.Management.Automation.ItemNotFoundException] {
$_.Exception.GetType().Name
}
The DivideByZeroException is basically just the InnerException of RuntimeException, and theoretically, the InnerExceptions could be endlessly nested:
catch {
$exception = $_.Exception
do {
$exception.GetType().Name
$exception = $exception.InnerException
} while ($exception)
}
BUT you can handle RuntimeException as a special case. Even PowerShell does so. Look at the first code example. The catch-block is reached even though the type of the inner exception is specified.
You could do something similar yourself:
catch {
$exception = $_.Exception
if ($exception -is [System.Management.Automation.RuntimeException] -and $exception.InnerException) {
$exception = $exception.InnerException
}
$exception.GetType().Name
}
NOTE that you need one try-catch per command, if you want to catch both exceptions. Else the 2nd will not be executed if the 1st one fails. Also you have to specify $ErrorActionPreference to "Stop" to catch also non-terminating exceptions.
Are you trying to catch specific error types like this?
https://blogs.technet.microsoft.com/poshchap/2017/02/24/try-to-catch-error-exception-types/
Related
I'd like to show someone an example on how to catch non terminating errors in PowerShell.
I created this function:
# This will write a non-terminating error
function NonTerminatingErrorExample {
Param($i = 5)
if ($i -gt 4) {
Write-Error "I expected a value less or equal to 4!"
}
Write-Host "However, I can still continue the execution"
}
However, I can't catch it with -ErrorAction Stop
Try {
NonTerminatingErrorExample -ErrorAction Stop
} Catch {
Write-Host "Now you see this message."
}
I never get the catch block returned. Why?
NonTerminatingErrorExample : I expected a value less or equal to 4!
In Zeile:32 Zeichen:5
+ NonTerminatingErrorExample -ErrorAction Stop
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,NonTerminatingErrorExample
However, I can still continue the execution
Your function isn't a cmdlet and therefore doesn't support -ErrorAction. Add [CmdletBinding()] to the body to make it one and the parameter will be supported. You can see the difference if you Get-Help both versions: as a function, there is no [<CommonParameters>].
I'm writing a Powershell script which executes one of the steps in my build/deploy process, and it needs to run some actions on a remote machine. The script is relatively complex, so if an error occurs during that remote activity I want a detailed stack trace of where in the script the error occurred (over and above the logging that is already produced).
The problem arises in that Invoke-Command loses stack trace information when relaying terminating exceptions from a remote machine. If a script block is invoked on the local machine:
Invoke-Command -ScriptBlock {
throw "Test Error";
}
The required exception detail is returned:
Test Error
At C:\ScriptTest\Test2.ps1:4 char:2
+ throw "Test Error";
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Test Error:String) [], RuntimeException
+ FullyQualifiedErrorId : Test Error
But if run remotely:
Invoke-Command -ComputerName $remoteComputerName -ScriptBlock {
throw "Test Error";
}
The exception stack trace points to the whole Invoke-Command block:
Test Error
At C:\ScriptTest\Test2.ps1:3 char:1
+ Invoke-Command -ComputerName $remoteComputerName -ScriptBlock {
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Test Error:String) [], RuntimeException
+ FullyQualifiedErrorId : Test Error
I can transport the exception back to the local machine manually:
$exception = Invoke-Command -ComputerName $remoteComputerName -ScriptBlock {
try
{
throw "Test Error";
}
catch
{
return $_;
}
}
throw $exception;
But re-throwing it loses the stack trace:
Test Error
At C:\ScriptTest\Test2.ps1:14 char:1
+ throw $exception;
+ ~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Test Error:PSObject) [], RuntimeException
+ FullyQualifiedErrorId : Test Error
If I write the exception to Output:
$exception = Invoke-Command -ComputerName $remoteComputerName -ScriptBlock {
try
{
throw "Test Error";
}
catch
{
return $_;
}
}
Write-Output $exception;
I get the correct stack trace information:
Test Error
At line:4 char:3
+ throw "Test Error";
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Test Error:String) [], RuntimeException
+ FullyQualifiedErrorId : Test Error
But as it's not on the Error stream it isn't picked up correctly by my build tools. If I try Write-Error, I have a similar problem to re-throwing the exception and the stack trace points to the wrong part of the script.
So my question is - how do I get Powershell to report the exception from a remote machine as if it had been raised locally, with the same stack trace information and on the Error stream?
When you run some code and it fails, you receive an ErrorRecord that reflects the code you (local computer) executed. So when you use throw "error" you can access the invocationinfo and exception for that code.
When you use Invoke-Command, you are not executing throw "error" anymore, the remote computer is. You (local computer) are executing Invoke-Command ...., which is why the ErrorRecord you get reflects that (and not the real exception like you wanted). This is the way it has to be since an exception may be coming from the scriptblock the remote comptuer executed, but it could just as well be an exception from Invoke-Command itself because it couldn't connect to the remote computer or something similar.
When the exception is originally thrown on the remote computer, Invoke-Command/PowerShell throws a RemoteException on the local computer.
#Generate errors
try { Invoke-Command -ComputerName localhost -ScriptBlock { throw "error" } }
catch { $remoteexception = $_ }
try { throw "error" }
catch { $localexception = $_ }
#Get exeception-types
$localexception.Exception.GetType().Name
RuntimeException
$remoteexception.Exception.GetType().Name
RemoteException
This exception-type has a few extra properties, including SerializedRemoteException and SerializedRemoteInvocationInfo which contains the information from the exception that was thrown in the remote session. Using these, you can receive the "internal" exception.
SerializedRemoteException: Gets the original exception that was thrown by the remote instance of Windows PowerShell.
SerializedRemoteInvocationInfo: Gets the invocation information for the remote instance of Windows PowerShell.
Sample:
#Show command that threw error
$localexception.InvocationInfo.PositionMessage
At line:4 char:7
+ try { throw "error" }
+ ~~~~~~~~~~~~~
$remoteexception.Exception.SerializedRemoteInvocationInfo.PositionMessage
At line:1 char:2
+ throw "error"
+ ~~~~~~~~~~~~~
You can then write a simple function to extract the information dynamically, ex:
function getExceptionInvocationInfo ($ex) {
if($ex.Exception -is [System.Management.Automation.RemoteException]) {
$ex.Exception.SerializedRemoteInvocationInfo.PositionMessage
} else {
$ex.InvocationInfo.PositionMessage
}
}
function getException ($ex) {
if($ex.Exception -is [System.Management.Automation.RemoteException]) {
$ex.Exception.SerializedRemoteException
} else {
$ex.Exception
}
}
getExceptionInvocationInfo $localexception
At line:4 char:7
+ try { throw "error" }
+ ~~~~~~~~~~~~~
getExceptionInvocationInfo $remoteexception
At line:1 char:2
+ throw "error"
+ ~~~~~~~~~~~~~
Be aware that the SerializedRemoteExpcetion is shown as PSObject because of the serialization/deserialization during network transfer, so if you're going to check the exception-type you need to extract it from psobject.TypeNames.
$localexception.Exception.GetType().FullName
System.Management.Automation.ItemNotFoundException
$remoteexception.Exception.SerializedRemoteException.GetType().FullName
System.Management.Automation.PSObject
#Get TypeName from psobject
$remoteexception.Exception.SerializedRemoteException.psobject.TypeNames[0]
Deserialized.System.Management.Automation.ItemNotFoundException
I am sure someone with more experience can help but I would like to give you something to chew on in the mean time. Sounds like you want to be using throw since you are looking for terminating exceptions. Write-Error does write to the error stream but it is not terminating. Regardless of your choice there my suggestion is still the same.
Capturing the exception into a variable is a good start for this so I would recommend this block from your example:
$exception = Invoke-Command -ComputerName $remoteComputerName -ScriptBlock {
try
{
throw "Test Error";
}
catch
{
return $_;
}
}
$exception in this case should be a Deserialized.System.Management.Automation.ErrorRecord. You can send custom objects to throw...
You can also throw an ErrorRecord object or a Microsoft .NET Framework exception.
but in this case it does not work which is likely due to the deserialization. At one point I tried to create my own error object but some of the needed properties were read only so I skipped that.
PS M:\Scripts> throw $exception
Test Error
At line:1 char:1
+ throw $return
+ ~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Test Error:PSObject) [], RuntimeException
+ FullyQualifiedErrorId : Test Error
However just writing to the output stream gives you the correct information as you have seen.
PS M:\Scripts> $exception
Test Error
At line:4 char:9
+ throw "Test Error";
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Test Error:String) [], RuntimeException
+ FullyQualifiedErrorId : Test Error
$exception is an object will properties that contain all of this information so a possible way to get what you need would be to make a custom error string from the desired properties. However that turned out to be more work that it was worth. A simple compromise would be to use Out-String to convert that useful output so that it can be returned as an error.
PS M:\Scripts> throw ($return | out-string)
Test Error
At line:4 char:9
+ throw "Test Error";
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Test Error:String) [], RuntimeException
+ FullyQualifiedErrorId : Test Error
At line:1 char:1
+ throw ($return | out-string)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Test Error
At ...Test Error
:String) [], RuntimeException
+ FullyQualifiedErrorId : Test Error
At line:4 char:9
+ throw "Test Error";
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Test Error:String) [], RuntimeException
+ FullyQualifiedErrorId : Test Error
So now we have a proper terminating error with information from both relative to the scriptblock and to where in the calling code the error is generated. You will see some repetition obviously but maybe this will be a start for you.
If there is other information you need specifically I would delve into the $exception object with Get-Member to see if you can find something specific that you are looking for. Some notable properties here would be
$exception.ErrorCategory_Reason
$exception.PSComputerName
$exception.InvocationInfo.Line
$exception.InvocationInfo.CommandOrigin
The last one would read something like this.
PS M:\Scripts> $exception.InvocationInfo.PositionMessage
At line:4 char:9
+ throw "Test Error";
+ ~~~~~~~~~~~~~~~~~~
Hoping someone with more experience can comment on this, my comment seems to have gone overlooked.
I found this article which offers some insight into using Enter-PSSession
Or even better, create a persistent session
$session = New-PSSession localhost
Invoke-Command $session {ping example.com}
Invoke-Command $session {$LASTEXITCODE}
To do debugging using tracing, remember that you need to set the VerbosePreference, DebugPreference etc in the remote session
Invoke-Command $session {$VerbosePreference = ‘continue’}
Invoke-Command $session {Write-Verbose hi}
Consider the script
$someComplexCondition = $false
if ($someComplexCondition)
{
Write-Error -Message "Some complex condition"
}
else
{
Write-Error -Message "Other complex condition"
}
When I run it it says
C:\Dev> .\MyScript.ps1
C:\Dev\MyScript.ps1 : Other complex condition
At line:1 char:15
+ .\MyScript.ps1 <<<<
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,MyScript.ps1
And I noticed that Write-Error always reports itself as at line:1
According to the stack trace it looks like it's line #1 because it was called from the shell and more interesting char:15 because
".\MyScript.ps1 ".Length -eq 15
If we change file name, this char:15 will be changed accordingly.
The question is how to get the actual line when the error occurred.
In our case I would like to get line:9
When there is an actual error, generated by Powershell you get an object of the type ErrorRecord :
$error[0] | Get-Member
TypeName: System.Management.Automation.ErrorRecord
When you use this same ErrorRecord with Write-Error, you don't get an object :
$myError = Write-Error -ErrorRecord $Error[0]
PS C:\> $MyError | Get-Member
gm : You must specify an object for the Get-Member cmdlet.
At line:1 char:12
+ $MyError | gm
+ ~~
+ CategoryInfo : CloseError: (:) [Get-Member], InvalidOperationException
+ FullyQualifiedErrorId : NoObjectInGetMember,Microsoft.PowerShell.Commands.GetMemberCommand
So now, we know that Write-Error will not give us anything that we can reuse later.
You can use the "Throw" statement, instead :
$someComplexCondition = $false
if ($someComplexCondition)
{
Throw "Some complex condition"
}
else
{
Throw "Other complex condition"
}
Then, when you run the script, the error gives you the line number and character number of the start of the "Throw" statement :
C:\Test-Error.ps1
Other complex condition
At C:\Test-Error.ps1:9 char:5
+ Throw "Other complex condition"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Other complex condition:String) [], RuntimeException
+ FullyQualifiedErrorId : Other complex condition
Here, it is line number 9.
I basically have this simple powershell script that executes an ssrs report url and saves it to the network.
It usually runs fine, but sometimes it times out and when it does, it still says it succeeded. I've tried a handful of things with no luck.
A simplified version of my script looks like this:
-----------------------------
function RunReport($url,$outputfile) {
# // create a request
[Net.HttpWebRequest] $req = [Net.WebRequest]::create($url)
$req.Method = "GET"
$req.Timeout = 600000 # = 10 minutes
# // Set credentials
$req.UseDefaultCredentials = $true
#echo "Getting Response"
[Net.HttpWebResponse] $result = $req.GetResponse()
[IO.Stream] $stream = $result.GetResponseStream()
#[IO.StreamReader] $reader = New-Object IO.StreamReader($stream)
[System.IO.FileStream]$writeStream = New-Object System.IO.FileStream($outputfile, [System.IO.FileMode]::Create);
# // write to file
[byte[]]$buffer = new-object byte[] 4096
[int]$total = [int]$count = 0
do
{
$count = $stream.Read($buffer, 0, $buffer.Length)
$writeStream.Write($buffer, 0, $count)
} while ($count -gt 0)
$writeStream.Close()
#$stream.flush()
$stream.Close()
}
$url=...
$outputfile=...
IF(RunReport "$url" "$outputfile")
{Write-Host "Success"}
ELSE
{Write-Host "Failed"}
-------------------------------
I've tried stuff like this with no luck:
RunReport "$url" "$outputfile"
If($?)
{Write-Host "Success"}
ELSE
{Write-Host "Failed"}
and
RunReport "$url" "$outputfile"
If($? -eq true)
{Write-Host "Success"}
ELSE
{Write-Host "Failed"}
The timeout error I'm dealing with is:
Exception calling "GetResponse" with "0" argument(s): "The operation has timed out"
At C:\data\powershell\script.ps1:9 char:49
+ [Net.HttpWebResponse] $result = $req.GetResponse <<<< ()
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException
You cannot call a method on a null-valued expression.
At C:\data\powershell\script.ps1:10 char:48
+ [IO.Stream] $stream = $result.GetResponseStream <<<< ()
+ CategoryInfo : InvalidOperation: (GetResponseStream:String) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
You cannot call a method on a null-valued expression.
At C:\data\powershell\script.ps1:18 char:38
+ $count = $stream.Read <<<< ($buffer, 0, $buffer.Length)
+ CategoryInfo : InvalidOperation: (Read:String) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
You cannot call a method on a null-valued expression.
At C:\data\powershell\script.ps1:23 char:14
+ $stream.Close <<<< ()
+ CategoryInfo : InvalidOperation: (Close:String) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
Any help would be greatly appreciated. I assume this should be fairly easy, just don't have the correct syntax ? Thanks
Maybe you can use try/catch to deal with that part like this:
try {
[Net.HttpWebResponse] $result = $req.GetResponse()
}
catch {
return 1
}
You can apply the same technique in other places where you suspect code is not doing what it is supposed to do like:
try {
[System.IO.FileStream]$writeStream = New-Object System.IO.FileStream($outputfile, [System.IO.FileMode]::Create);
}
catch {
return 1
}
Once you detect where the issue is happening, you could then look into exception like
catch { write-warning $_ ; exit 1 }
The 'cannot call a method on a null-valued expression' errors are all stemming from the fact that the .GetResponse method is failing with a timeout, leaving the $result variable unassigned. The simplest change would be to put the rest of the script (after "$result = $req.GetResponse()") into an "if ($result) {}" block. You can then use a "then {}" block to do your error handling.
A more advanced method would be to use try {} and catch {} blocks, to catch the actual timeout exception and handle it properly.
You have two problems:
The timeouts (and the cascading errors from that)
Your RunReport function doesn't ever return anything.
For #2, you can't test something that doesn't exist. So make your function return some kind of success/fail indicator to the caller.
For #1, when you call GetResponse() you need to wrap it in a try/catch block, catch the exception, and exit the function with the appropriate status returned to the caller.
You might want to look at this SO post about calling SSRS using SOAP methods as well.
I'm working on a PowerShell library that automates some network management operations. Some of these operations have arbitrary delays, and each can fail in unique ways. To handle these delays gracefully, I'm creating a generic retry function that has three main purposes:
Execute an arbitrary command (with parameters)
If it fails in a recognized way, try it again, up to some limit
If it fails in an unexpected way, bail and report
The problem is item #2. I want to be able to specify the expected exception type for the command. How can I do this in PowerShell?
Here's my function:
Function Retry-Command {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True, Position=0)]
[String] $name,
[Parameter(Mandatory=$True, Position=1)]
[String] $scriptBlock,
[String[]] $argumentList,
[Int] $maxAttempts=3,
[Int] $retrySeconds=10,
[System.Exception] $retryException=[System.Management.Automation.RuntimeException]
)
$attempts = 1
$keepTrying = $True
$cmd = [ScriptBlock]::Create($scriptblock)
do {
try {
&$cmd #argumentList
$keepTrying = $False
Write-Verbose "Command [$commandName] succeeded after $attmpts attempts."
} catch [$retryException] {
$msg = "Command [$commandName] failed. Attempt $attempts of $maxAttempts."
Write-Verbose $msg;
if ($maxAttempts -gt $attempts) {
Write-Debug "Sleeping $retrySeconds"
Start-Sleep -Seconds $retrySeconds
} else {
$keepTrying = $False
Write-Debug "Reached $attempts attempts. Re-raising exception."
Throw $_.Exception
}
} catch [System.Exception] {
$keepTrying = $False
$msg = "Unexpected exception while executing command [$CommandName]: "
Write-Error $msg + $_.Exception.ToString()
Throw $_.Exception
} finally {
$attempts += 1
}
} while ($True -eq $keepTrying)
}
I call it like this:
$result = Retry-Command -Name = "Foo the bar" -ScriptBlock $cmd -ArgumentList $cmdArgs
But this is the result:
Retry-Command : Cannot process argument transformation on parameter 'retryException'.
Cannot convert the "System.Management.Automation.RuntimeException" value of type "System.RuntimeType" to type "System.Exception".
At Foo.ps1:111 char:11
+ $result = Retry-Command <<<< -Name "Foo the bar" -ScriptBlock $cmd -ArgumentList $cmdArgs
+ CategoryInfo : InvalidData: (:) [Retry-Command], ParameterBindin...mationException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError,Retry-Command
This seems to be saying that the type of [System.Management.Automation.RuntimeException] is not itself a [System.Exception], but is instead a [System.RuntimeType] which makes sense.
So, how do I specify the type of the exception to be caught?
It's not possible to use a variable as a catch criteria, it has to be a type-object (or something), everything else gives you an error. A workaround would be something like this:
#You can get the name of the exception using the following (or .Name for just the short name)
#PS > $myerr.Exception.GetType().Fullname
#System.UnauthorizedAccessException
function test {
param(
#Validate that specified name is a class that inherits from System.Exception base class
[ValidateScript({[System.Exception].IsAssignableFrom([type]$_)})]
$ExceptionType
)
try {
#Test-script, Will throw UnauthorizedAccessException when not run as admin
(Get-Content C:\test.txt) | % { $_ -replace 'test','lol' } | Set-Content C:\test.txt
}
catch [System.Exception] {
#Check if exceptiontype is equal to the value specified in exceptiontype parameter
if($_.Exception.GetType() -eq ([type]$ExceptionType)) {
"Hello. You caught me"
} else {
"Uncaught stuff: $($_.Exception.Gettype())"
}
}
}
A few tests. One with non-existing type, then with non-exception type, and finally a working one
PS > test -ExceptionType system.unaut
test : Cannot validate argument on parameter 'ExceptionType'. Cannot convert the "system.unaut" val
ue of type "System.String" to type "System.Type".
At line:1 char:21
+ test -ExceptionType system.unaut
+ ~~~~~~~~~~~~
+ CategoryInfo : InvalidData: (:) [test], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,test
PS > test -ExceptionType String
test : Cannot validate argument on parameter 'ExceptionType'. The "[System.Exception].IsAssignableF
rom([type]$_)" validation script for the argument with value "String" did not return true. Determin
e why the validation script failed and then try the command again.
At line:1 char:21
+ test -ExceptionType String
+ ~~~~~~
+ CategoryInfo : InvalidData: (:) [test], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,test
PS > test -ExceptionType UnauthorizedAccessException
Hello. You caught me