Using the snippet below, I'm trying to find a way to suppress the "Attempted to divide by zero" error, and be left with just the custom error message in $err. I've tried combinations of try/catch, Invoke-Command/Expression, redirecting 2>$null, but in every case the error stream still contains the "attempted to divide by zero" message.
function func1 {
[CmdletBinding()]
param()
func2 -ErrorAction SilentlyContinue -ErrorVariable er
if ($er.Count -ge 1) {
Write-Error -Message "Custom error message!"
}
}
function func2 {
[CmdletBinding()]
param()
1/0
}
func1 -ErrorAction SilentlyContinue -ErrorVariable err
$err
populates $err with two items:
Attempted to divide by zero.
At line:15 char:3
+ 1/0
+ ~~~
+ CategoryInfo : NotSpecified: (:) [], RuntimeException
+ FullyQualifiedErrorId : RuntimeException
func1 : Custom error message!
At line:18 char:1
+ func1 -ErrorAction SilentlyContinue -ErrorVariable err
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,func1
Thanks so much for your thoughts!
Unfortunately the solution is somewhat tricky, because the error that occurs in func2 is invariably recorded in the $err variable passed to -ErrorVariable in the func1 invocation, and is already stored there when the func2 call returns.
Therefore you need to manually remove it:
Replace:
if ($er.Count -ge 1) {
Write-Error -Message "Custom error message!"
}
with:
if ($er.Count -ge 1) {
if ($PSBoundParameters.ContainsKey('ErrorVariable')) {
# Remove the most recently added error from the $err variable.
$PSCmdlet.GetVariableValue($PSBoundParameters['ErrorVariable']).RemoveAt(0)
}
Write-Error -Message "Custom error message!"
}
The automatic $PSBoundParameters variable allows you to check if the common -ErrorAction parameter was specified and with what value (i.e., what target variable name).
The automatic $PSCmdlet variable's .GetVariableValue() method allows access to variables defined in the caller's scope, which enables retrieving the target variable value, which is a [System.Collections.ArrayList] instance whose .RemoveAt() method can be called to remove the most recently added error, which is at index 0.
Asides:
1/0 causes a statement-terminating error, whereas the common -ErrorAction parameter only acts on non-terminating errors (to catch terminating errors you need try / catch). However, because this statement-terminating error occurs inside a function, execution inside that function (potentially) continues, and to the caller the error is in effect a non-terminating one.
For a comprehensive overview of PowerShell's bewilderingly complex error handling, see GitHub docs issue #1583.
Related
Issue
When using -ErrorAction "SilentlyContinue", the error variable is not being populated as described in the following documentation: Handling Errors the PowerShell Way.
Question
How to prevent the error to be displayed as it is with -ErrorAction "Continue" yet still populate the error variable?
Bonus question
Is there a way to append the error to the error variable in order to store more than one of them?
MWE
Script
$ErrorActions = #("Continue", "SilentlyContinue", "Stop")
foreach ($ErrorAction in $ErrorActions) {
Write-Host -Object $ErrorAction -ForegroundColor "Green"
Get-Item -Path "C:\tmp\error1" -ErrorAction $ErrorAction -ErrorVariable "ErrorMessage"
Get-Item -Path "C:\tmp\error2" -ErrorAction $ErrorAction -ErrorVariable "ErrorMessage"
Write-Host -Object "ErrorVariable" -ForegroundColor "Yellow"
$ErrorMessage
}
Output
Continue
Get-Item : Cannot find path 'C:\tmp\error1' because it does not exist.
At C:\tmp\mwe.ps1:43 char:5
Get-Item -Path "C:\tmp\error1" -ErrorAction $ErrorAction -ErrorVa ...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
CategoryInfo : ObjectNotFound: (C:\tmp\error1:String) [Get-Item], ItemNotFoundException
FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetItemCommand
Get-Item : Cannot find path 'C:\tmp\error2' because it does not exist.
At C:\tmp\mwe.ps1:44 char:5
Get-Item -Path "C:\tmp\error2" -ErrorAction $ErrorAction -ErrorVa ...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
CategoryInfo : ObjectNotFound: (C:\tmp\error2:String) [Get-Item], ItemNotFoundException
FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetItemCommand
ErrorVariable
Get-Item : Cannot find path 'C:\tmp\error2' because it does not exist.
At C:\tmp\mwe.ps1:44 char:5
Get-Item -Path "C:\tmp\error2" -ErrorAction $ErrorAction -ErrorVa ...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
CategoryInfo : ObjectNotFound: (C:\tmp\error2:String) [Get-Item], ItemNotFoundException
FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetItemCommand
SilentlyContinue
ErrorVariable
Stop
Get-Item : Cannot find path 'C:\tmp\error1' because it does not exist.
At C:\tmp\mwe.ps1:43 char:5
Get-Item -Path "C:\tmp\error1" -ErrorAction $ErrorAction -ErrorVa ...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
CategoryInfo : ObjectNotFound: (C:\tmp\error1:String) [Get-Item], ItemNotFoundException
FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetItemCommand
Using Get-ChildItem as an example:
Get-ChildItem C:\nonexist -EA SilentlyContinue -EV silentErr
[bool]$silentErr # ==> True
As the documentation you linked to mentions, for -ErrorVariable and $ErrorActionPreference controls the following behavior when non-terminating errors occur (these do not affect terminating errors):
Continue: Display the error and continue execution.
SilentlyContinue: Hide error but still add it to the -ErrorVariable, if specified, and $error stack.
Stop: Throws an exception and halts execution. Exceptions may be caught and handled via a try / catch / finally block.
Non-terminating errors cannot be normally handled via try / catch / finally because an exception isn't thrown unless -ErrorAction is Stop.
Ignore: The error is swallowed with no indication it occurred. Neither -ErrorVariable or the $error stack is updated in this case.
Inquire: Ask the operator what to do if an error occurs.
Using the above knowledge, we can still rely on the $error automatic variable and the specified-ErrorVariable to be populated if the -ErrorAction is SilentlyContinue.
Note: You can also use the $ErrorView variable to further control how errors are displayed in the console.
To answer your sub-question, there is not a mechanism for giving a "named" error stack, but you can use the built-in $error stack to keep track of the errors which occur (and didn't occur under-EA Ignore). If you want to ensure you only have errors that occur at a certain point onward in your script/session/etc, you can call the following to clear the error variable:
$error.Clear()
Additional Information
Terminating Errors
Non-Terminating Errors
$ErrorActionPreference preference variable
$Error and $ErrorView automatic variables
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>].
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.
am trying to catch a specific exception in a powershell script. Thecodeblock underneath is trying to do this, when the given exception is being thrown the script should ignore it:
try
{
Stop-WebAppPool $webServerSettings.applicationPool
}
catch [System.InvalidOperationException]
{
}
Unfortunately this does not seem to work, I still am getting this response:
Stop-WebAppPool : Object on target path is already stopped.
At F:\Portal.Update.ps1:12 char:2
+ Stop-WebAppPool $webServerSettings.applicationPool
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [Stop-WebAppPool], InvalidOperationException
+ FullyQualifiedErrorId : InvalidOperation,Microsoft.IIs.PowerShell.Provider.StopAppPoolCommand
There are a few options I tried:
Passing the 'ErrorAction Stop' parameter value to the command. Now I do not have the error, but the script stops, that is not what I want.
Passing the 'ErrorAction SilentlyContinue' parameter value to the command. But this will only work when catching all types of exceptions, which I do not want because if the application pool does not exist, I want the exception to be thrown.
UPDATE:
I found a way to work around this problem: Just read out the $_.exception variable and with GetType() get the real exception type, but having a function in a catch does not seem to right way to go:
try
{
Stop-WebAppPool $webServerSettings.applicationPool
}
catch
{
if ($_.Exception.GetType().Name -eq "InvalidOperationException")
{
return
}
throw
}
Try,
try
{
Stop-WebAppPool $webServerSettings.applicationPool -ErrorAction Stop
}
Catch
{
"Unable to stop Web App Pool"
}
-ErrorAction Stop tells PowerShell to stop when it encounters an error; this suppresses the error output you were seeing.
I am getting the following non-terminating error in PowerShell. I want to catch this and write-host a generic statement, rather than see the error.
Here is the output:
You cannot call a method on a null-valued expression. At C:\PowerShell
Scripts\windowsUpdates.ps1:17 char:2
+ $a = $key.GetValue("LastSuccessTime")
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], ParentContainsErrorRec ordException
+ FullyQualifiedErrorId : InvokeMethodOnNull
My catch statement is currently not "catching" this.
Any help is appreciated.
I figured this out. I simply added if($variable -eq $null){write-host "message";}.
There are a few options
Depending on where you are getting the null variable from you could add
-ErrorAction Stop"
after the command so that it treats the non-terminating error as a terminating error.
If that's not an option you could throw an error to force your catch statement to catch the error
if($variable -eq $null){Throw("Variable is Null")}
Or you can make it so that all errors are treated as terminating errors so your catch statement will catch anything that is thrown
$ErrorActionPreference = "Stop"
For more information look at the MSDN documentation here: http://blogs.msdn.com/b/kebab/archive/2013/06/09/an-introduction-to-error-handling-in-powershell.aspx