I am using Publish-Module and one of the modules had a bad psd1 file. PowerShell threw an exception as expected. The call to Publish-Module is inside a try block but the error handling code in the catch block never ran. It appears that this error is not being caught.
There is another error that happens in this same PowerShell code where the module I am publishing already exists in the repository. When that error occurs the code in the catch block runs and processes the exception. Is there something different about the first exception that would cause the catch block to be bypassed?
Code snippet:
try {
Publish-Module -Path .\$moduleName -Repository MyRepo -NuGetApiKey ghehdue
"Module $moduleName published."
}
catch {
if ($_.Exception.Message -ilike "*cannot be published as the current version*is already available in the repository*") {
"The latest version of module $moduleName already exists in the repository."
}
else {
$exitCode += 1
Write-Error $_
}
}
}
Error that is not caught
Microsoft.PowerShell.Core\Test-ModuleManifest : The module manifest 'J:\Builds\
Jenkins\PROJECT_2456764.0\Applications\ALM\PSModules\MyCompany.Build\MyCompany.B
uild.psd1' could not be processed because it is not a valid Windows PowerShell
restricted language file. Remove the elements that are not permitted by the
restricted language:
At J:\Builds\Jenkins\PROJECT_2456764.0\Applications\ALM\PSModules\MyCompany.Bui
ld\MyCompany.Build.psd1:13 char:9
+ GUID = 'ccaa548f-8194-4cfa-a659-260f6ddc556b'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Unexpected token 'ccaa548f-8194-4cfa-a659-260f6ddc556b'
# Author of this module
Author = 'MyCompany'
# Company or vendor of this module
CompanyName = 'MyCompany' in expression or statement.
At J:\Builds\Jenkins\PROJECT_2456764.0\Applications\ALM\PSModules\MyCompany.Bui
ld\MyCompany.Build.psd1:13 char:9
+ GUID = 'ccaa548f-8194-4cfa-a659-260f6ddc556b'
+ ~
The hash literal was incomplete.
At J:\Builds\Jenkins\PROJECT_2456764.0\Applications\ALM\PSModules\MyCompany.Bui
ld\MyCompany.Build.psd1:19 char:25
+ CompanyName = 'MyCompany, Inc.'
+ ~
Missing argument in parameter list.
At J:\Builds\Jenkins\PROJECT_2456764.0\Applications\ALM\PSModules\MyCompany.Bui
ld\MyCompany.Build.psd1:118 char:1
+ }
+ ~
Unexpected token '}' in expression or statement.
At C:\Program Files
(x86)\WindowsPowerShell\Modules\PowerShellGet\1.0.0.1\PSModule.psm1:989 char:27
+ ... $module = Microsoft.PowerShell.Core\Test-ModuleManifest -Path $mani ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ResourceUnavailable: (J:\Builds\Jenki...Quip.Bui
ld.psd1:String) [Test-ModuleManifest], MissingMemberException
+ FullyQualifiedErrorId : Modules_InvalidManifest,Microsoft.PowerShell.Com
mands.TestModuleManifestCommand
Error that is caught
publish-module : The module 'DqCryptography' with version '1.0.2' cannot be published as the current version '1.0.2' is already available in the repository 'http://usas26:8624/nuget/PROJECTPowerShell/'.
At line:1 char:1
+ publish-module -Path DqCryptography -Repository PROJECTPowerShell - ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [Publish-Module], InvalidOperationException
+ FullyQualifiedErrorId : ModuleVersionIsAlreadyAvailableInTheGallery,Publish-Module
Inside the try block add an error action to the Publish-Module
Publish-Module -Path .\$moduleName -Repository MyRepo -NuGetApiKey ghehdue -ErrorAction Stop
Related
This question already has an answer here:
How to handle failed variable assignments in powershell? [duplicate]
(1 answer)
Closed 3 years ago.
I am trying to write a simple PowerShell code to create a registry key, then use TRY and CATCH to handle/catch any potential exceptions that may occur. As a test scenario, I am expecting to get "Script failed to create the registry key" if I modify the registry path. Unfortunately, the TRY/CATCH error handling function doesn't work for me and except the error itself nothing shows up in the console.
$NetBTpath = "HKLM:\System\CurrentControlSet\Services\NetBT\Parameters"
$RegValueName = "NodeType"
Try
{
if (((Get-ItemProperty $NetBTpath).PSobject.Properties.Name -contains $RegValueName) -ne "True")
{
New-ItemProperty -Path $NetBTpath -Name "NodeType" -Value 2 -PropertyType "dword"
}
}
Catch [System.Exception]
{
Write-warning "Script failed to create the registry key"
}
It works fine as long as the registry path is correct but if I rename the registry folder ...\NetBT\Parameters to ...\NetBT\Parameters1, I would only see:
Get-ItemProperty : Cannot find path 'HKLM:\System\CurrentControlSet\Services\NetBT\Parameters' because it does not exist.
At C:\temp\NetBT_RegConfig222.ps1:10 char:11
+ if (((Get-ItemProperty $NetBTpath).PSobject.Properties.Name -cont ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (HKLM:\System\Cu...etBT\Parameters:String) [Get-ItemProperty], ItemNotFoundExcep
tion
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetItemPropertyCommand
New-ItemProperty : Cannot find path 'HKLM:\System\CurrentControlSet\Services\NetBT\Parameters' because it does not exist.
At C:\temp\NetBT_RegConfig222.ps1:12 char:9
+ New-ItemProperty -Path $NetBTpath -Name "NodeType" -Value 2 ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (HKLM:\System\Cu...etBT\Parameters:String) [New-ItemProperty], ItemNotFoundExcep
tion
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.NewItemPropertyCommand
I have already tried to use only Catch {} as well as Catch [System.Management.Automation.ItemNotFoundException].
Please advice.
You need to add the -ErrorAction Stop switch to your Get-ItemProperty and New-ItemProperty lines. Sometimes commands will throw a non-fatal error, and the catch doesn't get invoked. To ensure that you will fall into your catch, add the above switch.
I have some Powershell code that takes the Apps running on an IIS server and finds the versions of .NET they are running.
$Apps = Get-WebApplication
foreach($App in $Apps) {
$binLocation = "$($App.physicalPath)\bin"
# get all dlls in bin folder
$dllFolder = Get-Item -Path $binLocation
$dlls = $dllFolder.GetFiles("*.dll")
# analyze dll .net version
$set = New-Object System.Collections.Generic.HashSet[String]
$dlls | ForEach-Object {
$set.Add([Reflection.Assembly]::ReflectionOnlyLoadFrom("$binLocation\$($_.Name)").ImageRuntimeVersion) | Out-Null
}
# print all dll .NET version
$App
$set
}
However when I run this on my server I get 2 types of error;
Exception calling "ReflectionOnlyLoadFrom" with "1" argument(s): "API restriction: The
assembly 'file:///D:\inetpub\wwwroot\OABS_ECRM\bin\WebGrease.dll' has already loaded from a
different location. It cannot be loaded from a new location within the same appdomain."
At line:13 char:74
+ $set.Add([Reflection.Assembly]::ReflectionOnlyLoadFrom ("$binLocation\$($_.Name ...
+ ~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : FileLoadException
and,
Exception calling "ReflectionOnlyLoadFrom" with "1" argument(s): "Could not load file or
assembly 'file:///D:\inetpub\wwwroot\Tablet\bin\epengine.dll' or one of its dependencies.
The module was expected to contain an assembly manifest."
At line:13 char:74
+ $set.Add([Reflection.Assembly]::ReflectionOnlyLoadFrom("$binLocation\$($_.Name ...
+ ~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : BadImageFormatException
I seem to get the .NET versions running on each app from the script but I was wondering what caused these errors and if they can be cleared. I need to solve this in powershell.
Loading multiple DLLs with the same identity into the same AppDomain will give you the first exception.
See my workaround at https://stackoverflow.com/a/62379741/920618.
Update 1:
Originally, I posted this with the title: "Scripts ignoring error handling in PowerShell module" as that is the current issue, however it seems more of a module issue, so I have renamed the title.
Update 2:
After a comment that made me question Azure cmdlets, I've tested with the most basic of scripts (added to the module) and the findings are the same, in that the error is not passed to the calling script, however, adding -errorVariable to Get-Service does return something (other than WriteErrorException) that I could probably harness in the handling of the error:
function Test-MyError($Variable)
{
Try
{
Get-Service -Name $variable -ErrorAction Stop -ErrorVariable bar
#Get-AzureRmSubscription -SubscriptionName $variable -ErrorAction Stop
}
Catch
{
Write-Error $error[0]
$bar
}
}
returns:
Test-MyError "Foo"
Test-MyError : Exception of type 'Microsoft.PowerShell.Commands.WriteErrorException' was thrown.
At line:3 char:1
+ Test-MyError "Foo"
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Test-MyError
The running command stopped because the preference variable "ErrorActionPreference" or common parameter is set to Stop: Cannot find any service with service name 'foo'.
However, if I run "Test-MyError" in ISE, then call the function, I get:
Test-MyError "Foo"
Test-MyError : Cannot find any service with service name 'Foo'.
At line:3 char:1
+ Test-MyError "Foo"
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Test-MyError
The running command stopped because the preference variable "ErrorActionPreference" or common parameter is set to Stop: Cannot find any service with service name 'Foo'.
So I am not sure what is happening when running "Test-MyError" in ISE and calling it, against it being dot-sourced in the PSM1 file and then calling it?
Do I now have to use -ErrorVariable and handle on that?
Original Question:
I have two functions in a module: Get-Subscription and Get-AllSubscriptions. Each function sits in its own PS1 file and the PSM1 file dot-sources them. The module seems fine as the module scripts are accessible using intelisense and the module loads without issue. I've used this structure in many modules and I haven't come across this problem before. (Although I wonder if MS have changed the way modules work in PS 5.1 as I have noticed using FunctionsToExport='x','y','Z' and Export-ModuleMember don't seem to behave the same way as they used to.)
Get-AllSubscriptions calls Get-Subscription.
If I am not logged into Azure, Get-Subscription should throw an error which is handled, prompting me to log in. This works as expected, if I call Get-Subscription from the Get-Subscription.ps1.
However, when I call Get-Subscription from the a new PS1 file, Get-AllSubscriptions or from the powershell console, it doesn't work. It iterates all the way through the do..until loop, without "handling" the errors as I would expect. On each iteration, it seems to throw a generic error:
Get-Subscription : Exception of type 'Microsoft.PowerShell.Commands.WriteErrorException' was thrown.
However, I do see the last error, Get-Subscription : Unable to find requested subscription after 3 login attempts.
If I execute Get-Subscription in ISE, then call Get-Subscription in a new PS1 file or from Get-AllSubscriptions, it works as expected, however, once I re-import the module (Import-Module AzureVnetTools -Force -Verbose), it goes back to the incorrect behaviour.
If I dot-source Get-Subscription, inside the caller script, it works, but why? This is what should happen with the module's PSM1.
Can anyone help me work out what I am doing wrong here?
(PS 5.1, Windows 7)
Get-Subscription:
function Get-Subscription
{
[cmdletbinding()]
Param
(
[string]$SubscriptionName,
[string]$UserName,
[string]$code
)
$c=1
Write-Verbose "Checking access to '$SubscriptionName' with user '$UserName'..."
Do
{
Write-Verbose "Attempt $c"
Try
{
$oSubscription = Get-AzureRmSubscription -SubscriptionName $SubscriptionName -ErrorAction Stop -WarningAction SilentlyContinue
Write-Verbose "Subscription found: $($oSubscription.SubscriptionName)."
}
Catch
{
if($error[0].Exception.Message -like "*Please verify that the subscription exists in this tenant*")
{
Write-Verbose "Cannot find subscription '$SubscriptionName' with provided credentials."
$account = Login-AzureRmAccount -Credential (Get-Credential -UserName $Username -Message "Subscription '$SubscriptionName' user' password:")
}
elseif($error[0].Exception.Message -like "*Run Login-AzureRmAccount to login*")
{
Write-Verbose "No logged in session found. Please log in."
$account = Login-AzureRmAccount -Credential (Get-Credential -UserName $Username -Message "Subscription '$SubscriptionName' user' password:")
}
else
{
Write-Error $error[0]
}
}
$c++
}
until(($oSubscription) -or ($c -eq 4))
if($c -eq 4)
{
Write-Error "Unable to find requested subscription after $($c-1) login attempts."
break
}
$oSubscription | Add-Member -MemberType NoteProperty -Name Code -Value $code
$oSubscription
}
Get-AllSubscriptions:
function Get-AllSubscriptions
{
[cmdletbinding()]
param
(
[string]$MasterSubscription,
[string]$MasterSubscriptionCode,
[string]$MasterSubscriptionUsername,
[string]$ChildSubscription,
[string]$ChildSubscriptionCode,
[string]$ChildSubscriptionUsername
)
Write-Verbose "Getting all subscriptions..."
$oAllSubscriptions = #()
$oMasterSubscription = Get-Subscription -SubscriptionName $MasterSubscription -UserName $MasterSubscriptionUsername -code $MasterSubscriptionCode -Verbose
$oChildSubscription = Get-Subscription -SubscriptionName $ChildSubscription -UserName $ChildSubscriptionUsername -code $ChildSubscriptionCode -Verbose
$oAllSubscriptions = ($oMasterSubscription,$oChildSubscription)
$oAllSubscriptions
}
Test:
$splat2 = #{
SubscriptionName = "SomeSubscription"
Code = "S02"
Username = "some.user#somewhere.com"
}
#Write-Output "Dot-source:"
#. "D:\Temp\PS.Modules\AzureVnetTools\functions\public\Get-Subscription.ps1"
Get-Subscription #splat2 -verbose
Output:
Get-Subscription #splat2 -verbose
VERBOSE: Checking access to 'SomeSubscription' with user 'some.user#somewhere.com'...
VERBOSE: Attempt 1
Get-Subscription : Exception of type 'Microsoft.PowerShell.Commands.WriteErrorException' was thrown.
At line:7 char:1
+ Get-Subscription #splat2 -verbose
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-Subscription
VERBOSE: Attempt 2
Get-Subscription : Exception of type 'Microsoft.PowerShell.Commands.WriteErrorException' was thrown.
At line:7 char:1
+ Get-Subscription #splat2 -verbose
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-Subscription
VERBOSE: Attempt 3
Get-Subscription : Exception of type 'Microsoft.PowerShell.Commands.WriteErrorException' was thrown.
At line:7 char:1
+ Get-Subscription #splat2 -verbose
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-Subscription
Get-Subscription : Unable to find requested subscription after 3 login attempts.
At line:7 char:1
+ Get-Subscription #splat2 -verbose
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-Subscription
AzureVnetTools.psm1
#Get public and private function definition files.
$Public = #( Get-ChildItem -Path $PSScriptRoot\Functions\Public\*.ps1 -ErrorAction SilentlyContinue )
$Private = #( Get-ChildItem -Path $PSScriptRoot\Functions\Private\*.ps1 -ErrorAction SilentlyContinue )
#Dot source the files
Foreach($import in #($Public + $Private))
{
#write-error $import.fullname
Try
{
#Write-Host "Dot-sourcing file: $($import.fullname)."
. $import.fullname
}
Catch
{
Write-Error -Message "Failed to import function $($import.fullname): $_"
}
}
Export-ModuleMember -Function $Public.Basename
AzureVnetTools.psd1 (Relevant section):
FunctionsToExport = '*'
-ErrorAction Stop -WarningAction SilentlyContinue
Is it a warning that's being thrown instead of an error?
So my specific problem was that I was relying on handling the error and doing something based on that. The problem was caused by the way PowerShell's Write-Error works (or not) as I learned from the reply here, given by #Alek.
It simply wasn't passing the actual error back to the calling script. As #Alex suggested, I replaced Write-Error with $PSCmdlet.WriteError(). Although this didn't totally work.
In the Catch{} block, I then changed $error[0] to $_ and the full error was returned to the calling script / function.
I went one further and wrote a reusable function, added to my module:
function Write-PsError
{
[cmdletbinding()]
Param
(
[Exception]$Message,
[Management.Automation.ErrorCategory]$ErrorCategory = "NotSpecified"
)
$arguments = #(
$Message
$null #errorid
[Management.Automation.ErrorCategory]::$ErrorCategory
$null
)
$ErrorRecord = New-Object -TypeName "Management.Automation.ErrorRecord" -ArgumentList $arguments
$PSCmdlet.WriteError($ErrorRecord)
}
Which seems to be working well at the moment. I especially like the way intellisense picks up all the ErrorCategories. Not sure what or how ISE (PS 5.1 / Win 7) does that. I thought I was going to have to add my own dynamic parameter.
HTH.
I want to catch the exception that occurs, when adding a DNS record with Add-DnsServerResourceRecordA in PowerShell with the switch -CreatePTR, but no reverse lookupzone exists.
But there is no error. If I provoke the error a simple menu pops up and informs me about the situation. But independent from the -ErrorAction switch the $error variable does not receive an error. What's my fault?
Thanks for your reply.
PS C:\Users\xyz>> Add-DnsServerResourceRecordA -Name "test-mwi4" -IPv4Address 1.1.1.1 -CreatePtr -ZoneName contoso.biz -ErrorAction Continue
Add-DnsServerResourceRecordA : Failed to create PTR record. Resource record test-mwi4 in zone mn-man.biz on server MNDEMUCDC010 is created successfully, but corresponding PTR record could not be created.
At line:1 char:1
+ Add-DnsServerResourceRecordA -Name "test-mwi4" -IPv4Address 1.1.1.1 -CreatePtr - ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (test-mwi4:root/Microsoft/...ResourceRecordA) [Add-DnsServerResourceRecordA], CimException
+ FullyQualifiedErrorId : WIN32 9715,Add-DnsServerResourceRecordA
Thank you!
In order to use this command in try catch block use below code:
Try
{
Add-DnsServerResourceRecordA -Name "test-mwi4" -IPv4Address 1.1.1.1 -CreatePtr -ZoneName contoso.biz -ErrorAction Stop
}
Catch
{
Write-Host "Error while adding pointer record:`n$($Error[0].Exception.Message)"
}
$Error[0] returns:
$Error[0]
Add-DnsServerResourceRecordA : Failed to get the zone information for
contoso.biz on server HYDLPT487.
At line:3 char:5
+ Add-DnsServerResourceRecordA -Name "test-mwi4" -IPv4Address 1.1.1 ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (test-mwi4:root/Microsoft/...Resou
rceRecordA) [Add-DnsServerResourceRecordA], CimException
+ FullyQualifiedErrorId : WIN32 1722,Add-DnsServerResourceRecordA
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}