Skip to content

Cmdlet.ThrowTerminatingError() is not affected by -ErrorAction #14819

Closed

Description

If a cmdlet uses ThrowTerminatingError() then that ErrorRecord is not affected by the common -ErrorAction parameter. However, it is affected by $ErrorActionPreference. This inconsistency is not a regression from Windows PowerShell 5.1, but is a cause of lots of user confusion. Note that -ErrorAction Break DOES work because the code explicitly checks for that, but other values are not checked.

Steps to reproduce

Invoke-WebRequest https://foo.lskdjf -ErrorAction ignore

Expected behavior

Nothing

Actual behavior

Error printed

Environment data

Name                           Value
----                           -----
PSVersion                      7.2.0-preview.3
PSEdition                      Core
GitCommitId                    7.2.0-preview.3
OS                             Darwin 20.3.0 Darwin Kernel Version 20.3.0: Thu Jan 21 00:07:06 PST 2021; root:xnu-7195.81.3~1/RELEASE_X86_64
Platform                       Unix
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Activity

  1. added
    Needs-TriageThe issue is new and needs to be triaged by a work group.
    Review - CommitteeThe PR/Issue needs a review from the PowerShell Committee
    WG-Enginecore PowerShell engine, interpreter, and runtime
    and removed
    Needs-TriageThe issue is new and needs to be triaged by a work group.
    on Feb 16, 2021
  2. jborean93 commented on Feb 16, 2021

    @jborean93
    Collaborator

    I've always been confused as to what WriteError() and ThrowTemrinatingError() was for. I always assumed the latter was meant for exceptions and shouldn't be affected by either -ErrorAction or $ErrorActionPreference but it would be good to get some further clarification in the docs or at least the .NET docs for these methods.

  3. mklement0 commented on Feb 17, 2021

    @mklement0
    Contributor
  4. SeeminglyScience commented on Feb 17, 2021

    @SeeminglyScience
    Contributor

    I do think a change here would in the long run be an overall positive change, and I'd like to see it personally.

    I'm not sure what you could actually change here without causing some real weird breaks though. It seems like a significantly higher risk than what y'all would typically go for.

  5. mklement0 commented on Feb 17, 2021

    @mklement0
    Contributor

    Here's a possible way forward:

    • Eliminate statement-terminating errors altogether and have only one kind of terminating error: runspace-terminating ones (what I've been calling "script-terminating" error for simplicity, given that not every PowerShell user knows or needs to know what a runspace is); that is, .ThrowTerminatingError() would then terminate the runspace by default as well, just like throw already does.

      • Honestly, I'm not even sure if statement-terminating errors were ever meant to be introduced as such. Certainly, their elimination would make this particular headache go away and greatly simplify error handling overall.
    • Given the breaking nature of this change, it needs to be opt-in, via a new preference variable, say $PSErrorHandling, with enumeration values Legacy (default, old behavior) and Standard (new behavior), following the pattern proposed for the breaking change needed to fix argument-passing to external programs - see Native invocation using ArgumentList #14692 (comment)

      • This would additionally provide the opportunity to properly integrate error handling with respect to native applications, instead of introducing a separate $PSNativeCommandErrorAction preference variable - see the native error-handling RFC: https://github.com/PowerShell/PowerShell-RFC/pull/277/files
    GitHub
    Link to original: #261
  6. bpayette commented on Feb 25, 2021

    @bpayette
    Contributor

    Some history/context:

    Michael Klement (@mklement0) wrote:

    I'm not even sure if statement-terminating errors were ever meant to be

    They are absolutely by design. Statement terminating exceptions are intended to mimic shell semantics. An error in a pipeline (optionally) writes a message then terminates the pipeline and sets $?.

    Now PowerShell error objects are much richer than traditional shell errors so one of the scenarios we had in mind was error recovery/remediation based on the contents of the error records. For example, if you're deleting a lot of files and there are some failures, the error records record which files were not deleted and why. This allows you to take additional actions in the script to delete those files.

    Error disposition is also much richer in PowerShell through -ErrorAction and $ErrorActionPreference. Unfortunately rich semantics also imply additional complexity. The more choices you can make implies more things to think about.

    PowerShell 1.0 also had trap (taken collectively from the Posix shell, Perl 6 and VB.) Trap has the interesting characteristic that it allows you to resume execution after an exception at the next statement in the current lexical context.

    Unfortunately trap was difficult to use when dealing with typical "programmer" scenarios so in PowerShell V2 we introduced try/catch . Now you have even more choices. This becomes especially tricky when you start to (inevitably) blend the various semantics.

    And one more dimension of complexity - expressions vs commands. For performance reasons, an expression is not evaluated in the context of the pipeline processor so it has slightly different error semantics.

    Philosophically, our goal was such that if you wrote shell-style code, then you would get (enhanced) shell semantics and if you wrote programmer-style code (implied by the use of try/catch) then you would get (mostly) programmer semantics.

    This doesn't mean that there aren't bugs (there are) or that we got the design totally right. However, tweaking the existing semantics without a total scenario review is unlikely to make things better, just different implying new confusion not less confusion. I do think it would be interesting to do the full scenario review and see if a new "error mode" makes sense. (Note: if we did an error mode it would have to be a compile-time switch i.e. #requires rather than yet another runtime preference variable.)

    I've been calling "script-terminating" error for simplicity

    Yes - this is the preferred terminology. 'Runspace terminating' is inaccurate as the error doesn't "terminate" the runspace, just the currently executing set of statements i.e. the script.

  7. mklement0 commented on Feb 25, 2021

    @mklement0
  8. SeeminglyScience commented on Feb 25, 2021

    @SeeminglyScience
    Contributor
    • thread-terminating is technically clearer

    The full phrase "the current thread of execution" is unfortunately sorta needed there as it doesn't actually terminate the thread (in 99.99% of cases). It's difficult to come up with good terminology for this. Maybe "call stack terminating"? That's going to be mostly meaningless to a lot of folks probably.

  9. mklement0 commented on Feb 25, 2021

    @mklement0
  10. mklement0 commented on Feb 25, 2021

    @mklement0
    Contributor

    Bruce Payette (@BrucePay), there are multiple problematic aspects with PowerShell's current error handling, so why not take this opportunity to perform the full scenario review you propose:

    • The lack of proper documentation and official terminology, which Our Error Handling, Ourselves - time to fully understand and properly document PowerShell's error handling MicrosoftDocs/PowerShell-Docs#1583 addresses, but it's obviously no substitute for official documentation.

    • The lack of integration of native-executable calls into PowerShell's error handling.

    • The complexity stemming from multiple constructs, as you've discussed.

    • The surprising asymmetries around the two types of terminating errors that exist, which is what prompted creation of this issue:

      • -ErrorAction only affects non-terminating errors, while $ErrorActionPreference also affects statement-terminating errors (promotes them to script-terminating ones).
      • A binary cmdlet can only throw a statement-terminating error.
      • PowerShell code can only throw a script-terminating error, with throw (unless you go out of your way - available in an advanced script/functions only - to call $PSCmdlet.ThrowTerminatingError(), which is both obscure and cumbersome).

    As you state, a change in this area would inevitably be breaking and require an opt-in; a concern regarding a #require directive would again be to ensure lexical scoping rater than the usual dynamic one that affects all descendant scopes - the same issue that just came up in the context of #14747 (comment).


    Statement terminating exceptions are intended to mimic shell semantics.

    It is the non-terminating errors that mimic shell semantics, while offering additional features (log of errors in $Error, per-command error collection with -ErrorVariable).

    cmd.exe offers no terminating errors at all, whereas POSIX-compatible shells offer the equivalent of $ErrorActionPreference = 'Stop', i.e. script-terminating errors via set -e (which is often eschewed in favor or per-call error handling with || exit; we now offer something similar, but with more cumbersome syntax (|| $(exit $LASTEXITCODE) or, if throw is sufficient, || $(throw)).

    These shells themselves have no concept of statement-terminating errors (although individual executables can choose to abort pipeline processing by closing their end of the pipe prematurely).

    PowerShell's statement-terminating errors offer a way to abort a pipeline instantly, as a whole, in case an error condition so severe is encountered that further pipeline processing makes no sense. Statement-terminating errors also occur in expressions, such as when .NET methods throw an exception.

    The question is whether it is helpful to by default terminate only the pipeline / statement being executed.

    To me, it isn't: given that it indicates an error condition that is severe, the more sensible behavior is to terminate the script.

    That is, we can make do without the concept of a statement-terminating error and only have one type of terminating error: the script-terminating error:

    • This amounts to a substantial conceptual simplification that automatically makes the above-mentioned asymmetries go away.
    • Those who want a script-terminating error to be in effect only statement(-block)-terminating can and should use try / catch (or trap, but my sense is that it is rarely used anymore).

    I believe that this, along with integrating external-executable calls (treat nonzero exit codes as non-terminating errors, but escalate to script-terminating with $ErrorActionPreference = 'Stop', with the selective exceptions discussed in the Native Command Error Handling RFC), would go a long way toward solving the current problems.

  11. SeeminglyScience commented on Feb 25, 2021

    @SeeminglyScience
    Contributor

    Perhaps the better approach is to give up the quest for a more accurate term, given that the official one should be the friendly "script-terminating", and that with the right interpretation of "script" (a PowerShell thread's implicit top-level script block, right?) it is even defensible as technically accurate.

    When talking about implementation, "pipeline" is probably the word to use there. It would be confusing to use in documentation, but a pipeline is sort of the rawest measurement of a unit of execution in PowerShell.

    For example, this code:

    PowerShell.Create().AddCommand("Get-ChildItem").Invoke()
    
    // or more similar to what ConsoleHost does:
    using var pipeline = runspace.CreatePipeline();
    pipeline.Commands.Add(new Command("Get-ChildItem", isScript: false));
    pipeline.Invoke();

    There's no script there, it's just a pipeline processor that looks up the command info and creates a command processor.

    None of this is to say that "script-terminating" isn't the right term to use for docs, I honestly don't know. Just giving you the full picture.

    Are you referring to the fact that in an interactive session the underlying exception is caught, which thereby prevents termination?

    I would say that is generally a good way to look at it, and probably a good way to present it when not talking about implementation specifics.

    If we were being the absolute strictest of nitpicky, the thread is only terminated when one of these happens:

    1. The Runspace is set to PSThreadOptions.ReuseThread and is GCed
    2. The Runspace is set to PSThreadOptions.UseNewThread and the current top level pipeline is completed (successfully or otherwise)
    3. The Runspace is set to PSThreadOptions.UseCurrentThread and an exception is not caught by the caller

    ReuseThread is what most hosts (including ConsoleHost) use. Since execution is actually handled on a different thread controlled by the Runspace object, the actual thread itself won't really terminate.

    (P.S. I wrote this before your second post but then got distracted. I still haven't read the second one FYI, so sorry if my delayed answer equated to more work or something)

  12. 2 remaining items

  13. SteveL-MSFT commented on Mar 3, 2021

    @SteveL-MSFT
    MemberAuthor

    @PowerShell/powershell-committee reviewed this, we agree that the intent of ThrowTerminatingError() is not to be affected by either -ErrorAction nor $ErrorActionPreference which is only intended to elevate non-terminating errors to be statement terminating and this is a bug. However, we are concerned about breaking existing scripts that may be depending upon this behavior which has been there since at least PowerShell 2.0. Since 7.2 is a LTS, we would not take a change here, but can revisit for 7.3 at the earliest.

  14. added
    Committee-ReviewedPS-Committee has reviewed this and made a decision
    and removed
    Review - CommitteeThe PR/Issue needs a review from the PowerShell Committee
    on Mar 3, 2021
  15. mklement0 commented on Mar 4, 2021

    @mklement0
    Contributor

    Steve Lee (@SteveL-MSFT)

    intended to elevate non-terminating errors to be statement terminating and this is a bug.

    No, -ErrorAction Stop and $ErrorActionPreference = 'Stop' elevate errors to script-terminating (fatal ) ones, not to statement-terminating ones - and that is part of the asymmetry discussed previously.

    That is, if we resolved this simply by making $ErrorActionPreference = 'Stop' no longer act on statement-terminating errors, we'd just be introducing a different asymmetry:

    • It would mean taking away the ability to use $ErrorActionPreference = 'Stop' to abort on errors of any type, which is a common and useful idiom, because statement-terminating errors would then be exempt.

    • Conversely, this means that the only way to elevate a statement-terminating error to a script-terminating (fatal) one would then be to wrap individual statements (or groups) explicitly in try { ... } catch { throw } - or to use trap { break }, but trap seems to have fallen out of favor.

    I may have mentioned this before, but if we simplified the model to only two types of errors: fatal (the current script-terminating ones) and non-fatal (non-terminating), these asymmetries and headaches would go away.
    (The word "terminating" is now problematic, given the ambiguity of the scope of the termination).

    Since a breaking change is required either way, I suggest breaking things in a more useful manner.

  16. SteveL-MSFT commented on Mar 4, 2021

    @SteveL-MSFT
    MemberAuthor

    Michael Klement (@mklement0) I agree it may be worth while to consider a proposal that is breaking, but would avoid user confusion for the next "10 years". We should certainly continue that conversation.

    The @PowerShell/powershell-committee did discuss the need to use try..catch or trap to elevate an error, but that is the current originally intended design and we were not considering in that discussion a major break

  17. mklement0 commented on Mar 6, 2021

    @mklement0
    Contributor

    Understood, Steve Lee (@SteveL-MSFT), but my point was that even limiting ourselves to fixing the bug (from the perspective of the original design intent):

    • results in (different) behavior that is still unhelpful.
    • does amount to a major break.

    To put it differently: we have a major break on our hands either way (which will require an opt-in); the bug fix is not worth making, and I suggest taking the opportunity to get it right for the next "10 years".

  18. jhoneill commented on Apr 15, 2023

    @jhoneill

    Following discussions in #19500 there's a clarification / pedantic point to add.
    That issue concerns something different - if the current $errorActionPreference in a function, set by -ErrorAction or inherited from the caller, is silently continue throw is considered to be caught and handled silently, and execution continues.
    But it also strayed into -ErrorAction working / not working and ErrorActionPreference.

    I think the initial post mis-explains the behaviour

    consider this code

    function foo {
    [CmdletBinding()]
    Param( $p)
    $ErrorActionPreference ="SilentlyContinue"
    if ($P -gt 1) {
        $err_rec = [System.Management.Automation.ErrorRecord]::new("Oops","",7,$null) 
        $PSCmdlet.ThrowTerminatingError($err_rec)
    }
    "Something bad happens if $p >1 "
    }
    

    It sets $ErrorActionPreference - so, according to the initial description, we would expect the error to be supressed but in practice:

    > foo 2
    foo: Oops
    

    Whatever this error is returned to does what $errorActionPreference says it should do. So if the function is called from a second function:

    function bar {
    [CmdletBinding()]
    Param( $p)
    $ErrorActionPreference
    foo $p
    "returned to bar"
    }
    

    Execution continues back in the calling function after printing the error if the preference is continue.

     bar 2
    Continue
    foo: 
    Line |
      19 |  foo $p
         |  ~~~~~~
         | Oops
    returned to bar
    

    Execution continues back in the calling function WITHOUT printing the error if the preference is SILENTLYcontinue.

    bar 2 -ErrorAction SilentlyContinue
    SilentlyContinue
    returned to bar
    

    And it stops without printing "returned to bar" if the preference is STOP.

    As already pointed out by Michael Klement (@mklement0) et al. "terminating" errors have multiple varieties

    If I change the first function to use throw the message "returned to bar" isn't printed

    function foo {
    [CmdletBinding()]
    Param( $p)
    if ($P -gt 1) {
        $err_rec = [System.Management.Automation.ErrorRecord]::new("Oops","",7,$null) 
        throw $err_rec
    }
    "Something bad happens if $p >1 "
    }
    
    bar 2 
    Continue
    InvalidOperation: 
    Line |
       6 |      throw $err_rec
         |      ~~~~~~~~~~~~~~
         | Oops
    

    but if the error-action-preference is silentlycontinue the throw is caught immediately.

    bar 2 -errorAction SilentlyContinue
    SilentlyContinue
    Something bad happens if 2 >1
    returned to bar
    

    This creates problems for people writing and calling functions.
    For writers calling $pscmdlet.ThrowTerminatingError() side steps -ErrorAction (think of that as "do X when an error is thrown to you" and the ThrowTerminatingError as "throw an error to your caller") , and throw stops other code running in the calling script - so without try{} Catch{} any clean up is missed.
    For callers -ErrorAction silently continue works for some errors, but for a throw it can cause the function code to continue and do something harmful (e.g. in
    $x = get-thingName; if (-not $x) {throw} ; delete-thing -name "$x*"
    -errorAction SilentlyContinue would delete every thing) .

  19. jhoneill commented on Apr 18, 2023

    @jhoneill

    Continuing discussions in #19500 The following idea occurred to me.

    1. -ErrrorAction is a common parameter like , for example -WhatIf
    2. The way that some cmdlets raise errors is not impacted by $ErrorActionPreference, but that does change how the caller processes the error that it receives. Therefore in a cmdlet like Invoke-WebRequest, -ErrorAction does nothing.
    3. Therefore a [cmdletbinding(SupportsErrorAction=$false)] / [cmdletbinding(LocalErrorAction='Stop')] declaration would allow us to prevent the common parameter appearing then it has no effect (as with SupportsShouldProcess and -WhatIf) and prevent $ErrorActionPreference defeating a throw statement.

    I haven't thought through all the possible wrinkles with this, but it might ease the problem.

  20. microsoft-github-policy-service commented on Nov 16, 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.

  21. microsoft-github-policy-service commented on Nov 16, 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.

  22. microsoft-github-policy-service commented on Nov 23, 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

    Committee-ReviewedPS-Committee has reviewed this and made a decisionResolution-No ActivityIssue has had no activity for 6 months or moreWG-Enginecore PowerShell engine, interpreter, and runtime

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions