Skip to content

Throw outside Try\Trap acts like a non-terminating error #19500

Closed

Description

@dronkoff

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

Activity

  1. mklement0 commented on Apr 13, 2023

    @mklement0
  2. jhoneill commented on Apr 14, 2023

    @jhoneill

    Michael Klement (@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{}

    PowerShell (or PWSH if you prefer). Devops (especially Azure Devops), Photography, and general thoughts
  3. mklement0 commented on Apr 14, 2023

    @mklement0
    Contributor

    Thanks, James O'Neill (@jhoneill) - I had already minimized my earlier comment after realizing I hadn't addressed the specific question.

    Indeed, the translation of -ErrorAction SilentlyContinue into a function-local $ErrorActionPreference = 'SilentlyContinue' variable is the culprit.

  4. mklement0 commented on Apr 14, 2023

    @mklement0
    Contributor

    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 emit: 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 James O'Neill (@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.

  5. dronkoff commented on Apr 14, 2023

    @dronkoff
    Author

    To be honest, I see the inconsistency only in the behaviour mentioned in the issue. That fact that -ErrorAction does not hide terminating errors looks pretty consistent in the two types of errors paradigm (terminating and non-teminating). The paradigm itself can be questioned, but this is nor the point of this issue. 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.

    https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_throw?view=powershell-7.3

    The throw keyword causes a terminating error. You can use the throw keyword to stop the processing of a command, function, or script.

    https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables?view=powershell-7.3#erroractionpreference

    Determines how PowerShell responds to a non-terminating error, an error that doesn't stop the cmdlet processing.

    https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_commonparameters?view=powershell-7.3#-erroraction

    Determines how the cmdlet responds to a non-terminating error from the command.

    Describes the Throw keyword, which generates a terminating error.
    Variables that customize the behavior of PowerShell.
    Describes the parameters that can be used with any cmdlet.
  6. jhoneill commented on Apr 14, 2023

    @jhoneill

    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 -

  7. mklement0 commented on Apr 15, 2023

    @mklement0
    Contributor

    Vladimir (@dronkoff)

    The fundamental inconsistency is that -ErrorAction and $ErrorActionPreference should be equivalent, but aren't.

    A further inconsistency is that the behind-the-scenes translation of the former into the latter in advanced scripts/functions is built on the assumption of this equivalence, which doesn't hold - it is the source of your problem.

    The behavior of try / catch and trap is an entirely separate issue that isn't part of your original post and from what I can tell not relevant to this discussion.

    As an aside: That the documentation doesn't distinguish between statement-terminating and script-terminating errors is a source of perennial confusion, which MicrosoftDocs/PowerShell-Docs#1583 is meant to address (I have now updated it to reflect the problem at hand).
    Also note that the $ErrorActionPreference description still states: "Determines how PowerShell responds to a non-terminating error"

  8. jhoneill commented on Apr 15, 2023

    @jhoneill

    Michael Klement (@mklement0) I think there two different things, and this one is not about calls to
    $PSCmdlet.ThrowTerminatingError looking at the $ErrorActionPreference from the parent scope instead of the current one.

    This is the if the ErrorActionPreference is silentlycontinue then throw is considered to be immediately caught and handled.

  9. mklement0 commented on Apr 15, 2023

    @mklement0
    Contributor

    James O'Neill (@jhoneill), agreed - and I certainly didn't mean to imply that this is about $PSCmdlet.ThrowTerminatingError - I was simply trying to paint the bigger picture, to show the full scope of the overall complexity and terminology confusion.
    (Also, $PSCmdlet.ThrowTerminatingError only creates a statement-terminating error and is therefore not an alternative to throw.)

    More fundamentally, what this is about is the inappropriate translation of -ErrorAction SilentlyContinue into a script/function-local $ErrorActionPreference = 'SilentlyContinue' variable, which constitutes a "leaky" implementation detail, if you will, given that they're not equivalent (though arguably always should have been, and the docs have at least historically falsely claimed that they are, and in part still do).

  10. mklement0 commented on Apr 15, 2023

    @mklement0
    Contributor

    As for a workaround for the second problem at hand, which we've been focusing on:

    If the intent is to make the function-internal throw trigger a script-terminating (fatal-by-default) error irrespective of what -ErrorAction value is passed, set the function-local $ErrorActionPreference variable to 'Continue' (or, perhaps less paradoxically, 'Stop') if -ErrorAction was actually used, which you can check for with $PSBoundParameters.ContainsKey('ErrorAction')):

    function Throw-Example
    {
        [CmdletBinding()]
        param()
        # Will be silenced by -ErrorAction SilentlyContinue.
        Write-Error "Non-terminating error"
        # If -ErrorAction was used, we don't want it to apply to our `throw` statement
        # and want the latter to generate a *script*-terminating error, as usual.
        # We do, however, want to honor a true $ErrorActionPreference value set 
        # by the caller.
        if ($PSBoundParameters.ContainsKey('ErrorAction')) {
          $ErrorActionPreference = 'Continue'
        }
        Throw "This is my custom terminating error"
        "Line after the throw" 
    }
    
    # Now produces no output, except for the error message from the 
    # script-terminating `throw` statement.
    Throw-Example -ErrorAction SilentlyContinue

    Also note the puzzling asymmetry between Continue and SilentlyContinue:

    While the names suggests that both continue - suggesting that the only difference is whether they continue silently -
    Continue actually aborts processing overall in the face of throw, whereas only SilentlyContinue continues, if set via $ErrorActionPreference.

  11. mklement0 commented on Apr 15, 2023

    @mklement0
    Contributor

    As for a workaround for the first problem at hand - simply make sure that $ErrorActionPreference is set to Continue or Stop before calling throw.

    I hadn't paid attention to the first problem earlier, though it is covered by the previous comments, and again highlights the fundamental inconsistency between -ErrorAction and $ErrorActionPreference that is the root problem.

    So let me try a succinct summary that covers both problems:

    • $ErrorActionPreferences = 'SilentlyContinue' silences and ignores throw statements, because - unlike what the documentation states - it applies to all error types, not just non-terminating ones.

      • Therefore, the behavior is as de-facto designed - not as documented.
    • -ErrorAction SilentlyContinue:

      • as seen by binary cmdlets indeed only applies to non-terminating errors emitted by a cmdlet, i.e. it does not silence terminating errors (this part is documented correctly in principle, but doesn't mention the problem with implemented-in-PowerShell cmdlets) , which, however, are only ever statement-terminating errors, meaning that only the enclosing statement (expression, pipeline) is terminated and execution continues with the next statement in the caller's scope (the only way to prevent that is with an ambient $ErrorActionPreferences = 'Stop', try / catch that uses throw, or trap that uses break, all of which result in a script-terminating error).

      • as seen by (quasi) cmdlets implemented in PowerShell code, in effect behaves the same as $ErrorActionPreferences = 'SilentlyContinue', because - behind the scenes - PowerShell automatically translates -ErrorAction SilentlyContinue into the former.

        • Therefore, with $ErrorActionPreferences = 'SilentlyContinue' in effect - whether via truly setting this preference variable in the caller's scope or as the result of the behind-the-scenes translation - cmdlet-internal throw statements are silenced an ignored too, with the added ill-effect of continuing to run to completion even inside the cmdlet.
  12. jhoneill commented on Apr 15, 2023

    @jhoneill

    Michael Klement (@mklement0) I think we're both on the same page. There are so many "if this , but that" things around error handling.
    I've written about the difference between $PSCmdlet.ThrowTerminatingError and throw it's fair to say, I think, that the former is not widely known. Personally I think that ThrowTerminatingError is preferable instead of using throw, because (a) throw can be caught when it shouldn't be thanks to ErrorAction (b) throw can't know what it will prevent running in the code that called it.
    But throw is simpler and people understand it, where I'd need to explain why I was using something else.

    Before I understood the issue of some cmdlets throwing a terminating error and some writing to error and continuing I think it was the AD cmdlets which drove me mad for "not respecting -ErrorAction" they need an empty catch{} to silence errors- and I didn't get why you couldn't catch get-item "non-existant" (unless you had -error action stop). They errors look the same so the only way to find out which kind they are, AFAIK, is by testing :-(

    The action common parameters have always changed the local value for the preference variables and I'm OK with their doing so. But we get confusion when Error action changes what happens when a message is written to error (squashed or elevated to terminating), and what happens when one is thrown to a bit of code, but not when it is thrown by that code.
    Only testing tells us that -ea 0 is different between get-item "bogus" and invoke-webrequest "bogus"`

  13. mklement0 commented on Apr 15, 2023

    @mklement0
    Contributor

    The -*action common parameters have always changed the local value for the preference variables, and I'm OK with their doing so.

    That would make sense if this behind-the-scenes translation amounted to equivalent behavior - but this is clearly is not the case for the -ErrorAction / $ErrorActionPreference pair, as amply discussed.

    I've written about the difference between $PSCmdlet.ThrowTerminatingError and throw it's fair to say, I think, that the former is not widely known

    The former shouldn't have to be be known in the realm of PowerShell code.

    Just like Write-Error corresponds to .$PSCmdlet.WriteError(), you could argue that throw should correspond to $PSCmdlet.ThrowTerminatingError()

    Conversely, you could argue that if throw - by default - has the power to be fatal, why don't binary cmdlets have this capability?
    If only PowerShell code should have this ability, why is there no PowerShell-idiomatic analog to $PSCmdlet.ThrowTerminatingError() in order to effect a statement-terminating error only?

    Did the concept of statement-terminating errors that resume execution in the caller's scope ever make sense?
    I don't think so.

    It is no accident that there's confusion around the - unacknowledged in the documentation - bifurcation of what constitutes a terminating error: I don't think it was ever intentional or at least cleanly designed.

    That there's confusion even in the realm of binary cmdlets over when it is appropriate to report a non-terminating vs. a statement-terminating error, such as in the AD cmdlets, is a separate (also vexing) issue.

    Let me state it unequivocally:

    • PowerShell's error handling, in its current state, is fundamentally broken.

    • It cannot be fixed without fundamentally breaking backward compatibility.

    • A radical simplification is called for, involving:

      • the elimination of statement-terminating errors - so as to only have - truly - terminating (fatal) errors
      • making -ErrorAction / $ErrorActionPreference truly equivalent.
  14. jhoneill commented on Apr 16, 2023

    @jhoneill

    Michael Klement (@mklement0) I had to go out and returned to finish what I was typing, without seeing you had added more in the meantime. And there are some extra things you raise. We're (still , mostly) on the same page, but I'll get back on all of it in the morning.
    I do think we are have a problem around "it's broken", "fixing it will break other things", and Bruce's comment in one of the other issues that changing it will, in look likelihood replace one badness with and equal but different one.

  15. 3 remaining items

  16. mklement0 commented on Apr 17, 2023

    @mklement0
    Contributor

    It is exactly what happens in dos/windows batch files and unix shell scripts .

    Yes, cmd.exe and Unix (POSIX-compatible) shells only have one type of error - signaled via a nonzero exit code - and by default it is never fatal.

    Unix shells have set -e to request that any nonzero exit code abort the running script (but not its callers), but it comes with its own conceptual challenges around when a nonzero exit code is considered handled and should therefore not abort.

    A common idiom is to use ls nosuch || exit for aborting processing on a per-call basis in case of a non-zero exit code.

    We now have something similar, but less concise and convenient: it is hampered by the need to use $(...) and, in the case of external programs, if aborting should be done via exit, requires explicit use of $LASTEXITCODE: ls nosuch || $(exit $LASTEXITCODE)


    In principle, It is commendable that PowerShell tried to make error handling more sophisticated by introducing non-terminating vs. terminating errors:

    • Non-terminating ones are those where the command can at least potentially operate successfully on part of the input, with the option to deal with the input that caused errors later.

      • An example is Get-ChildItem -Recurse that encounters inaccessible directories during traversal.
    • A terminating error is one where a command encounters a fundamental problem that prevents it from doing any meaningful work.

      • These are much rarer than non-terminating errors, at least with respect to being reported by the command itself
        • An example (one you used elsewhere) is [timespan]::MaxValue | Start-Sleep
      • However, as noted, more commonly such terminating errors also occur with syntax errors (e.g. Start-Sleep -NoSuchSwitch ) or exceptions thrown by .NET methods (e.g., [int]::Parse('foo'))

    So far, these are command-internal concepts, and they only apply to PowerShell-native commands.

    Doesn't it stand to reason that such terminating errors, given their nature, should - unless handled - result in aborting processing overall rather than continuing with the next statement in the caller's scope?

    In short: It would have made sense to only ever introduce one kind of terminating error - one that is fatal by default - see #14819 (comment) and #14819 (comment)

    The current bifurcation - statement- vs. script-terminating - is unacknowledged as such, a perennial source of confusion, and riddled with inconsistencies and edge cases - just look at the sheer volume and complexity of information in

    Even if you're aware of all the pitfalls in principle, working around them is cumbersome and even remembering them all is a challenge.

  17. jhoneill commented on Apr 18, 2023

    @jhoneill

    Michael Klement (@mklement0) between us we have hijacked a thread, and we are going over a lot of ground we both know and have charted

    The behaviour is equivalent but very hard to see.

    • It is unreasonable to expect PowerShell users to use $PSCmdlet.ThrowTerminatingError()
    • The behavior is clearly not equivalent with respect to throw, which is the reasonable way to report a terminating error from PowerShell code.

    Neither of us is a good arbiter of reasonable :-) but $PSCmdlet.ThrowTerminatingError() is not well known or widely used and changing that isn't going to happen - and we will probably be stuck with throw.

    Equivalent depends where we look. If the command being called respects $ErrorActionPreference then it doesn't matter if it set by -ErrorAction or inherited from the caller. If the command doesn't respect it - which is what 14819 is about - then $ErrorActionPreference determines what happens. So if I say "cmdlet X/ function X does exactly the same whether you have $ErrorActionPreference set or use -ErrorAction" I am correct, if you say "when my code calls X , I see something different depending on whether I set $ErrorActionPreference or call x -erroraction" (because setting $ErrorActionPreference in my code changes the behaviour there which I wanted to change at source with ErrorAction) then you are also correct.

    That these two ways of reporting a terminating error actually mask a crucial, unacknowledged difference between the two fundamental types of terminating errors - statement-terminating vs. script-terminating (runspace-terminating, fatal) - is indicative of the fundamental brokenness of PowerShell's error handling.

    PowerShell tries to be two things which come into conflict: a .Net language and DOS batch files for the 21st century. I think this is a result of that, but I wouldn't categorize it is fundamentally broken .

    Take a line of code

    $a = 1/$x ; $b = 2/$x 
    

    What should happen if $x is zero. A "systems programmer" will say "the whole thing that contains it must terminate, at the first error, it's nuts to print the error and carry on, what might an empty "$a" (and $b) cause later.

    A bunch of places I've worked in use something like this

    Invoke-WebRequest web1 | out-null 
    Invoke-WebRequest web2 | out-null  
    dir \\file1\shareA | out-null 
    

    to check that all servers are up.
    Say to the people who wrote that "batch file" that either or both of the cmdlets they are calling stops the process at the first failed server and they will go back to back to cmd or bash shaking their heads. Ping.exe doesn't, curl doesn't, dir doesn't in a windows batch file. Why should they here ? We can put these people in a room with the systems programmers and see who wins that argument.

    We have "Normal" and "exceptional" messages (verbose/information and error/warning respectively) getting people to agree what kind of a message something should be isn't easy - mkdir when the directory exists, warning message ? (You asked for a directory, things are as you requested) or error message (non terminating) ? (You asked me to create something and I couldn't - maybe the name exists as a file).

    Get-Content when the file doesn't exist - terminating error? non-terminating? or warning ?
    Our systems programmer will say code with "$t = get-content Text.txt" should not run the next line if text.txt is not found
    Out batchfile writer will say Get-Content *.err > \logs\Todays_errors.err leaves yesterdays errors in the file if today was error free ?

    Trying to please people who (a) expect things to be consistent (b) want what they have used for 15 years keep working (c) Have a specific idea about what error behaviour should be is, IMHO an impossible task but I do have a partial solution.

  18. mklement0 commented on Apr 18, 2023

    @mklement0
    Contributor

    Note:

    • I know I'm (mostly) not really stating something new, but perhaps the various different angles are helpful to different readers.
      For brevity, I'll use the term "script cmdlet" to refer to advanced functions / scripts.

    • I feel that this comment captures the gist of this issue, without a discussion on how to fix the problem.


    If the command doesn't respect it 

    • No binary cmdlet respects it for (invariably statement-only-)terminating errors.
    • Every script cmdlet respects it, for all errors, in an inappropriate manner. (Only those rare birds that go to the trouble of using $PSCmdlet.ThrowTerminatingError() do not respect it, and therefore behave the same as binary cmdlets).

    A user that uses -ErrorAction sensibly expects it to apply to any kind of error (I suspect that most users don't even think about the terminating vs. non-terminating dichotomy),
    and that it doesn't work for binary cmdlets is a frequent source of confusion, as countless Stack Overflow questions can attest to, and as acknowledged in #14819.

    That it does apply to all errors reported from script cmdlets is a troubling inconsistency in itself, and the fact that a silenced throw statement
    results in running the script cmdlet to completion is even worse (the issue at hand).

    if I say "cmdlet X/ function X does exactly the same whether you have $ErrorActionPreference set or use -ErrorAction" I am correct

    That statement is not correct, and that's the problem.
    Quick recap:

    # -ErrorAction Stop is IGNORED, because the error is statement-terminating rather than non-terminating.
    & { Start-Sleep ([timespan]::MaxValue) -ErrorAction Stop; 'Why do we get here?' }
    
    # $ErrorActionPreference = 'Stop' is HONORED, because the preference variable applies to *all* errors.
    & { $ErrorActionPreference = 'Stop'; Start-Sleep ([timespan]::MaxValue); "Now we DON''T get here." }

     then $ErrorActionPreference determines what happens

    While that is, true, it doesn't help (and shouldn't be necessary) in the - quite common, if I were to guess - scenario where $ErrorActionPreference is at its default, Continue, and
    -ErrorAction SilentlyContinue or -ErrorAction Stop is used in individual calls.

    Neither works as expected with binary cmdlets when they raise a (statement)-terminating error:

    • SilentlyContinue doesn't silence.
    • -ErrorAction Stop doesn't abort processing overall

    Again, -ErrorAction and $ErrorActionPreference should be equivalent, but aren't. They only should differ in their scope (individual call vs. current all calls in the current and descendent scopes).

    What should happen if $x is zero.

    It should be considered a (script)-terminating error (the only kind of terminating error that should exist), and therefore should abort processing overall - unless caught.
    That makes sense just as much in shell programming as in compiled languages.

    that either or both of the cmdlets they are calling stops the process at the first failed server 

    Commands that operate on multiple inputs and encounter errors with individual inputs, should emit non-terminating errors, which - just like now - should by default neither abort the reporting command nor overall processing.

    The official guidelines on when to report which kind of error (which, sadly, don't discuss script-terminating vs. statement-terminating either) are at:
    https://learn.microsoft.com/en-us/powershell/scripting/developer/cmdlet/cmdlet-error-reporting

    A simple, true dichotomy (non-terminating vs. terminating==consistently-fatal-by-default errors only, as opposed to the current mess)
    strikes me as useful, and amounts to a step forward when compared to the everything-continues-by-default-no-matter-what-happens-model of traditional shells.

    Of course, users and, primarily, PowerShell command authors need to be aware of this dichotomy and use it appropriately.

    To me, it is what PowerShell should have done to begin with, but now the only way to implement requires seriously breaking backward compatibility.

  19. jhoneill commented on Apr 18, 2023

    @jhoneill

    That statement is not correct, and that's the problem. Quick recap:

    # -ErrorAction Stop is IGNORED, because the error is statement-terminating rather than non-terminating.
    & { Start-Sleep ([timespan]::MaxValue) -ErrorAction Stop; 'Why do we get here?' }
    
    # $ErrorActionPreference = 'Stop' is HONORED, because the preference variable applies to *all* errors.
    & { $ErrorActionPreference = 'Stop'; Start-Sleep ([timespan]::MaxValue); "Now we DON''T get here." }

    Sorry mike You keep making the same error here , and there just aren't enough hours in the day for me to keep this dialog going, so after this I'll you leave to it. We've got some agreement. Sadly we're stuck with what we have.

    In your second example setting $ErrorActionPreference Outside of Start-sleep changes what happens in script block when Start-sleep sends the terminating error back. It does not stop Start-sleep sending the terminating error

    It's easier to see with a function

    function reciprocal { 
    [cmdletbinding()]
    param ($x)
    Set-Variable -Scope 1 -Name ErrorActionPreference continue
    Write-Host "Error Action Preference here is $ErrorActionPreference"
    if ($x -eq 0) {
         $err_rec = [System.Management.Automation.ErrorRecord]::new("Zero causes an error","",5,$null) 
         $PSCmdlet.ThrowTerminatingError($err_rec)
    }
    else {return (1/$x)}
    }
    
    & {reciprocal 0 -ErrorAction Stop; 'Why do we get here?' }
    & { $ErrorActionPreference = 'Stop'; reciprocal 0 ; "We still get here." }
    

    knock the set-variable out of the function and try the last line again. See the difference ?

  20. mklement0 commented on Apr 18, 2023

    @mklement0
    Contributor

    In your second example setting $ErrorActionPreference Outside of Start-sleep changes what happens in script block when Start-sleep sends the terminating error back.

    Yes, and I certainly never meant to claim something different - but that is beside the point I was trying to make:

    My example was meant to demonstrate that - from the caller's perspective - setting $ErrorActionPreference vs. using -ErrorAction is not equivalent - despite what the docs claim and many users (sensibly) expect.

  21. mklement0 commented on Apr 18, 2023

    @mklement0
    Contributor

    On a meta note, James O'Neill (@jhoneill):

    While we've had prickly exchanges in the past, I feel that in recent exchanges we have - fortunately - settled into a collaborative relationship.

    Your most recent comment is a notable exception, with the following statements striking me as condescending / a power play:

    • "You keep making the same error here , and there just aren't enough hours in the day for me to keep this dialog going"

    • "Sorry mike" (faux familiarity - not that it matters in this context, but I don't go by "Mike")

    It's perfectly fine to opt out of a conversation that you perceive to be going nowhere (you feel you
    gave it your best shot, but you feel you weren't heard), but the way you framed it is likely to antagonize -
    not just me personally, but - if I were to guess - others, were you to respond to them in a a similar manner.

  22. jhoneill commented on Apr 19, 2023

    @jhoneill

    Michael Klement (@mklement0) I'm sorry about the name. I don't know why I thought you shortened it, but I did, I only needed to press @ to see my mistake.

    We now have a thread of nearly 7000 words where the OP could have been answered with a couple of sentences saying throw is treated as caught if errorActionPreference is set.

    I've said more than once whether two things are the same depends on whether you look at the command itself or the code that calls the command. Finally after saying the same thing multiple times and feeling I wasn't getting through.

    In your second example setting $ErrorActionPreference Outside of Start-sleep changes what happens in script block when Start-sleep sends the terminating error back.

    Yes, and I certainly never meant to claim something different - but that is beside the point I was trying to make:

    If you had that said 3000 words earlier we would both saved a lot of time (and you appear to be able to devote more time to this than me).

    Design decisions made a very long time ago mean -ErrorAction doesn't have always have the effects people reasonably expect it to have, and sometimes it has others things people do not expect it to have. Those decisions mean in some places when we want to catch an error we need to use -Ea STOP, and in others if we we want to ignore an error we must use try catch. And PowerShell defaulting to NOT terminating on a terminating error (but then having special cases) looks fine to someone who wrote .bat files and crazy to someone who compiles their code.

    What are we going to do about it? Realistically, nothing. I don't know what you're hoping to achieve with long posts with language like "fundamentally broken" or whether you see a danger that people who want to influence might just shrug and think "that's just him going off on one again..." but both of us would benefit from taking a moment to consider "Will saying that advance things or just antagonize". I'll try to do that more effectively with my replies to you going forward.

  23. mklement0 commented on Apr 19, 2023

    @mklement0
    Contributor

    If you had that said 3000 words earlier

    Conversely, I'm genuinely baffled by "depends on whether you look at the command itself or the code that calls the command", and why you would think that this apples-and-oranges juxtaposition is relevant to the discussion, and how it would lead you to express this as "cmdlet X/ function X does exactly the same whether you have $ErrorActionPreference set or use -ErrorAction"

    But since I seem to finally have gotten my message across, we don't need to dig deeper there.

    This fundamental disconnect seemingly kept the discussion going, and while it's never great to bury an issue in a lot of debate, sometimes lengthy debate is necessary to get to a shared understanding.
    If warranted in a given case, there's always the option of closing the original issue in favor of a new one that concisely reflects the shared understanding.

    both of us would benefit from taking a moment to consider "Will saying that advance things or just antagonize"

    Calling PowerShell's error handling fundamentally broken isn't inherently antagonizing, and it certainly isn't my intent:

    To me it is an apt summary of the state of affairs, based on extensive analysis, and it hopefully raises awareness that something needs to be done (more on that below).

    By contrast, statements such as "You keep making the same error here , and there just aren't enough hours in the day for me to keep this dialog going" serve no discernible purpose other than ego inflation at the expense of someone else.

    What are we going to do about it?

    • We are in agreement that fixing the current behavior isn't possible without seriously breaking backward compatibility.

    • Whether or not such a breaking change will ever happen, understanding exactly how things are currently broken is essential either way.

      • The issue at hand is a reminder that the depths of the current behavior still haven't been fully plumbed - at least for me, as I had never considered the specific ill effects of the common-parameter-to-preference-variable translation that I was aware of in principle.
    • Specifically, it is essential to the one that thing undoubtedly can be done:

  24. mklement0 commented on Apr 20, 2023

    @mklement0
    Contributor

    Vladimir (@dronkoff):

    Sorry for not seeing the try / trap angle earlier (it is mentioned in the issue's title and in your later comment).

    I think we've covered the angle of how things (unexpectedly) work in the absence of try / trap exhaustively.

    Inside Try \ Trap throw and -ErrorAction works as described in the documentation

    I don't think so; from what I can tell:

    • Premise:try / catch and trap truly only ever operate on terminating errors (of either kind)

    • They preempt $ErrorActionPreference and -ErrorAction (when applied to script cmdlets) with respect to the SilentlyContinue and Ignore values: If a terminating error occurs, they catch (trap) it regardless.

    To put it differently:

    • [Unexpectedly] Any innately terminating error that occurs in the scope of a try or trap statement is caught, irrespective of $ErrorActionPreference or -ErrorAction use (with script cmdlets) in the same or a descendant scope, even across script-file boundaries.

    • [As expected] Additionally, with $ErrorActionPreference or -ErrorAction set to Stop, non-terminating errors are also caught (due to having been promoted to terminating ones).

    To put it yet another way - and I realize we have come full circle here:

    • In the absence of try / catch and trap, $ErrorActionPreference applies to ALL errors, unlike what the docs say (as of this writing).

    • In the presence of these constructs, in effect it applies to NON-TERMINATING errors only (as claimed in the docs, but without mentioning these constructs, and without making the distinction).


    I think the confusion around the issue at hand and its many aspects perfectly exemplify the mind-bending complexity of PowerShell's current error-handling.

  25. jhoneill commented on Apr 20, 2023

    @jhoneill

    Calling PowerShell's error handling fundamentally broken isn't inherently antagonizing, and it certainly isn't my intent:

    To me it is an apt summary of the state of affairs, based on extensive analysis, and it hopefully raises awareness that something needs to be done (more on that below).

    By contrast, statements such as "You keep making the same error here , and there just aren't enough hours in the day for me to keep this dialog going" serve no discernible purpose other than ego inflation at the expense of someone else.

    I think it might be time to let this drop. If you don't think "fundamentally broken" or telling people they are engaged in ego inflation comes over as belligerent/antagonistic nothing from me will change your mind.
    There are things which are important in my life which have to fit into 24 hours and some of them have been dropped to bat things back and forth with you. I shouldn't have done that. If my admission of poor priority setting offends you, or a figure of speech used here doesn't work there, c'est la vie.

  26. mklement0 commented on Apr 20, 2023

    @mklement0
    Contributor

    The ego-inflation comment was just an aside; to focus on what matters:

    • There are fundamental problems with PowerShell's error handling, and at the very least they need to be properly documented, ideally also fixed in a future version that is allowed to break backward compatibility. Both tasks require a full understanding of how things currently (don't) work, and that requires - potentially lengthy - debate.

    • If you want to take offense at the wording "fundamentally broken", that is your choice - I don't think it's helpful.

    • What isn't helpful either is condescending meta commentary about how you choose to spend your time and the merits of engaging with someone else.

  27. microsoft-github-policy-service commented on Nov 23, 2023

    @microsoft-github-policy-service
    Contributor

    This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.

  28. microsoft-github-policy-service commented on Nov 30, 2023

    @microsoft-github-policy-service
    Contributor

    This issue has been marked as "No Activity" as there has been no activity for 6 months. It has been closed for housekeeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs-TriageThe issue is new and needs to be triaged by a work group.Resolution-No ActivityIssue has had no activity for 6 months or more

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions