Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
SteveL-MSFT opened this issue Feb 16, 2021 · 20 comments
Closed

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

SteveL-MSFT opened this issue Feb 16, 2021 · 20 comments
Labels
Committee-Reviewed PS-Committee has reviewed this and made a decision Resolution-No Activity Issue has had no activity for 6 months or more WG-Engine core PowerShell engine, interpreter, and runtime

Comments

@SteveL-MSFT
Copy link
Member

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
@SteveL-MSFT SteveL-MSFT added Needs-Triage The issue is new and needs to be triaged by a work group. Review - Committee The PR/Issue needs a review from the PowerShell Committee WG-Engine core PowerShell engine, interpreter, and runtime and removed Needs-Triage The issue is new and needs to be triaged by a work group. labels Feb 16, 2021
@SteveL-MSFT SteveL-MSFT added this to the 7.2-Consider milestone Feb 16, 2021
@jborean93
Copy link
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.

@mklement0
Copy link
Contributor

@jborean93, please see MicrosoftDocs/PowerShell-Docs#1583

@SeeminglyScience
Copy link
Collaborator

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.

@mklement0
Copy link
Contributor

mklement0 commented Feb 17, 2021

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

@bpayette
Copy link
Contributor

Some history/context:

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

@mklement0

This comment has been minimized.

@SeeminglyScience
Copy link
Collaborator

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

@mklement0

This comment has been minimized.

@mklement0
Copy link
Contributor

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

@SeeminglyScience
Copy link
Collaborator

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)

@mklement0
Copy link
Contributor

I appreciate the detailed explanation, @SeeminglyScience - I've hidden my previous comments on that topic.
My conclusion is that "script-terminating" should do for documentation purposes (and even for discussions here), and we can discuss how to explain what that means to a less technical audience in the context of authoring the proposed about_Error_Handling topic.

@SteveL-MSFT SteveL-MSFT removed this from the 7.2-Consider milestone Mar 3, 2021
@SteveL-MSFT
Copy link
Member Author

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

@SteveL-MSFT SteveL-MSFT added Committee-Reviewed PS-Committee has reviewed this and made a decision and removed Review - Committee The PR/Issue needs a review from the PowerShell Committee labels Mar 3, 2021
@mklement0
Copy link
Contributor

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

@SteveL-MSFT
Copy link
Member Author

@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

@mklement0
Copy link
Contributor

Understood, @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".

@jhoneill
Copy link

jhoneill commented Apr 15, 2023

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

@jhoneill
Copy link

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.

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

1 similar comment
Copy link
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.

@microsoft-github-policy-service microsoft-github-policy-service bot added Resolution-No Activity Issue has had no activity for 6 months or more labels Nov 16, 2023
Copy link
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
Labels
Committee-Reviewed PS-Committee has reviewed this and made a decision Resolution-No Activity Issue has had no activity for 6 months or more WG-Engine core PowerShell engine, interpreter, and runtime
Projects
None yet
Development

No branches or pull requests

6 participants