PowerShell: Throw outside Try\Trap acts like a non-terminating error
Prerequisites
- Write a descriptive title.
- Make sure you are able to repro it on the latest released version
- Search the existing issues.
- Refer to the FAQ.
- Refer to Differences between Windows PowerShell 5.1 and PowerShell.
Steps to reproduce
Considering that Throw is designed for generate Terminating errors and -ErrorAction and $ErrorActionPreference for Non-Terminating errors, why the following code does not terminate at 5?
$ErrorActionPreference = "SilentlyContinue"
1..10 |
ForEach-Object {
$_
if($_ -eq 5) {
throw "abc"
}
}
$ErrorActionPreference = "Continue"
function Throw-Example
{
[CmdletBinding()]
param()
"Line before the terminating error"
Throw "This is my custom terminating error"
"Line after the throw"
}
Throw-Example -ErrorAction SilentlyContinue
Expected behavior
Throw terminates the execution regardless of -ErrorAction or $ErrorActionPreference, like it does with the default Continue value.
Actual behavior
Throw does not terminates the execution like a non-terminating error when -ErrorAction or $ErrorActionPreference value is SilentlyContinue.
Error details
No response
Environment data
# $PSVersionTable
Name Value
---- -----
PSVersion 7.3.3
PSEdition Core
GitCommitId 7.3.3
OS Microsoft Windows 10.0.22621
Platform Win32NT
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
Visuals
No response
About this issue
- Original URL
- State: closed
- Created a year ago
- Comments: 30 (19 by maintainers)
If a terminating occurs inside a
try
then thecatch
always runs, regardless of the $errorActionPreference (which may have been set.We get odd behaviour when the $erroractionPreference is different in different scopes.
If I use the function I had before.
Now I can run
Expected . The throw stops everything.
As before the local value of
$errorActionPreference
inside the function - set by-ErrorAction
defeatsthrow
- same happens if the value inherits from the global scopeif I put the throw code inside a try block
Even though $errorActionPreference is silentlycontinue, catch sees the error.
And if I over-ride the preference inside the function.
The function exits at the throw, and the thrown error is treated as “caught” when control comes back to the caller with its different preference.
In effect a throw when the local value of erroractionPreference is silentlyContinue is treated as immediately caught UNLESS it is in a try block.
There are lot of additional ifs and buts -
In light of the above, let me try to provide the bigger picture and a summary of sorts:
In the world of binary cmdlets (cmdlets implemented as compiled .NET assemblies):
There are only two types of errors that binary cmdlets can omit: non-terminating (
.WriteError()
) and statement-terminating (.ThrowTerminatingError()
)throw
, or via-ErrorAction Stop
/$ErrorActionPreference = 'Stop'
[CmdletBinding()]
) and (b) if the - cumbersome -$PSCmdlet.ThrowTerminatingError()
method is used.A fundamental inconsistency is that
-ErrorAction
only acts on non-terminating errors (as emitted by binary cmdlets with.WriteError()
), whereas$ErrorActionPreference
affects all error types.In the realm of cmdlet-like commands implemented in PowerShell code, i.e. advanced scripts and functions - such as in the case at hand - this fundamental inconsistency gets even messier:
As the undesirable fallout from an unfortunate design decision - namely to translate the value passed to the
-ErrorAction
common parameter to a script/function-local$ErrorActionPreference
preference variable value when calling an advanced script/function --ErrorAction
effectively behaves like$ErrorActionPreference
and therefore acts on all error types.This shift in behavior depending on an implementation detail is bad enough (it shouldn’t matter whether the command you call happens to be implemented as a binary cmdlet or in PowerShell code), but its specific manifestation makes it worse:
-ErrorAction SilentlyContinue
not only ignores a script-terminating error (throw
) itself, but continues execution inside the advanced script/function; a minimal example:return
afterthrow
, but, needless to say, this requirement is obscure, easy to forget, and the larger issue remains: thethrow
statement doesn’t have the intended effect.Conversely,
-ErrorAction Stop
promotes statement-terminating errors to script-terminating ones, thereby preventing the script/function from continuing to execute, which it normally would (arguably, there shouldn’t be statement-terminating errors that aren’t also handled inside the script/function itself, but the fact remains that the behavior changes unexpectedly; to the caller, by default, such an internal statement-terminating error is an implementation detail that does not constitute a statement-terminating error).Resolving the fundamental inconsistency by making
-ErrorAction
too act on all error types, as discussed in #14819 - itself a major breaking change - wouldn’t address the problems specific to PowerShell code discussed above.@mklement0 I think you may have simplified to one case
-ErrorAction
sets$errorActionPreference
for the scope of a function and changes the behaviour of the throw statement. however PScmdlet.ThrowTerminatingError() - which is widely used in cmdlets - still prints the error and exists. -ErrorAction doesn’t work as expected on terminating errors thrown this way but does work with others.The interaction between the
throw
statement and theerroractionpreference
was designed this way, and puzzles just about everyone who sees it. I’ve been giving the advice for 10 years+ thatthrow
in a function should be followed byreturn
to protect against this, and I wrote a very long blog post about it here: https://jhoneill.github.io/powershell/2022/06/13/Errors.htmlIn #14819 the problem is the other way around. In
Invoke-WebRequest https://foo.lskdjf -ErrorAction ignore
The command can’t continue - the name didn’t resolve, we can’t connect to a port on a machine and make our request. And in some cases continuing would do damage. The user wants the error message to go away but it is still printed unless we wrap the command in
try {} catch{}