PowerShell: Throw outside Try\Trap acts like a non-terminating error

Prerequisites

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)

Most upvoted comments

The point is that throw and -ErrorAction behaves differently inside Try\Trap and outside of it. Inside Try\Trap throw and -ErrorAction works as described in the documentation, and outside of Try\Trap it does not work as expected or described in the documentation and that sounds like a bug. It’d be nice to have PG comments here, how it should work and why.

If a terminating occurs inside a try then the catch 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.

function foo {
[CmdletBinding()]
Param( $p)

if ($P -gt 1) {throw "oops"}
"Something bad happenes if $p >1 "
}

Now I can run

PS>  foo 2 ; "boo"
Exception:
Line |
   5 |  if ($P -gt 1) {throw "oops"}
     |                 ~~~~~~~~~~~~
     | oops

Expected . The throw stops everything.

PS>  foo 2 -ErrorAction SilentlyContinue ; "boo"
Something bad happenes if 2 >1
boo

As before the local value of $errorActionPreference inside the function - set by -ErrorAction defeats throw - same happens if the value inherits from the global scope

PS>  $ErrorActionPreference="SilentlyContinue"

PS>  foo 2 ; "boo"
Something bad happenes if 2 >1
boo 

if I put the throw code inside a try block

try {foo 2} catch {"oh dear"} ; "boo"
oh dear
boo

Even though $errorActionPreference is silentlycontinue, catch sees the error.

And if I over-ride the preference inside the function.

PS>  foo 2 -ErrorAction Continue ; "boo"
boo

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())

      • The third type of error - a script-terminating (runspace-terminating, fatal by default) can only be triggered either from PowerShell code, using throw, or via -ErrorAction Stop / $ErrorActionPreference = 'Stop'
      • Conversely, the only way for PowerShell code to trigger a statement-terminating error is from (a) inside an advanced script or function ([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:

        # !! throw is ignored, execution continues even inside the script block.
        & { [CmdletBinding()] param() 'before'; throw 'Outta here'; 'after' } -ErrorAction SilentlyContinue
        
        • Hence @jhoneill’s recommendation to place a return after throw, but, needless to say, this requirement is obscure, easy to forget, and the larger issue remains: the throw 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).

        # !! [int]::Parse('foo') is a *statement*-terminating error that, due to -ErrorAction Stop, 
        # !! now *aborts* processing inside the script block (too).
        & { [CmdletBinding()] param() 'before'; [int]::Parse('foo'); 'after' } -ErrorAction Stop
        

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

It is only -ErrorAction that is limited to non-terminating errors.

-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.

 function foo {
[CmdletBinding()]
Param( $p)

if ($P -gt 1) {throw "oops"}
"Something bad happenes if $p >1 "
}

> foo 1
Something bad happenes if 1 >1 

> foo 2
Exception: 
Line |
   5 |  if ($P -gt 1) {throw "oops"}
     |                 ~~~~~~~~~~~~
     | oops

> foo 2 -ErrorAction SilentlyContinue
Something bad happenes if 2 >1

The interaction between the throw statement and the erroractionpreference was designed this way, and puzzles just about everyone who sees it. I’ve been giving the advice for 10 years+ that throw in a function should be followed by return to protect against this, and I wrote a very long blog post about it here: https://jhoneill.github.io/powershell/2022/06/13/Errors.html

In #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{}

<div>The 'definitive' Terminating and Non-Terminating errors in PowerShell.</div><div>PowerShell (or PWSH if you prefer). Devops (especially Azure Devops), Photography, and general thoughts</div>