GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — setting error action preference in finally clause affects terminating parameter binding errors · Issue #15726 · PowerShell/PowerShell

setting error action preference in finally clause affects terminating parameter binding errors #15726

New issue
Copy link
New issue
Copy link
Closed
Closed
setting error action preference in finally clause affects terminating parameter binding errors#15726
Copy link
Labels
Issue-BugIssue has been identified as a bug in the productIssue has been identified as a bug in the productResolution-Won't FixThe issue won't be fixed, possibly due to compatibility reason.The issue won't be fixed, possibly due to compatibility reason.WG-Enginecore PowerShell engine, interpreter, and runtimecore PowerShell engine, interpreter, and runtime

Description

@dl2n
Dan Lovinger (dl2n)
opened on Jul 6, 2021
Issue body actions

Prerequisites

  • Write a descriptive title.
  • Make sure you are able to repro it on the latest released version
  • Search the existing issues.
  • Refer to the FAQ.
  • Refer to Differences between Windows PowerShell 5.1 and PowerShell.

Steps to reproduce

I’m stumbling into a detail of error handling that seems bugged. It is consistent between Windows PS 5.1 and PS 7.x, modulo how the errors are presented on the console.

• function with a try/finally
• set ErrorActionPreference = Stop at the top of the try and (intended case) reset EAP = Continue in finally
• Issue

A parameter binding error, Write-Error and Throw out of a try/finally do indeed result in a terminating error. If the finally does not reset the EAP, all three terminate the function.

If the finally resets the EAP = Continue, the parameter binding error is downgraded to a non-terminating error and the function continues. Write-Error and Throw are not downgraded and terminate the function as expected. It appears that
changing the EAP affects one class of errors in the course of the throw, while not affecting others.

In the repro, note that:

  • "f" throws out of the finally (does not emit "after")
  • "f -reset" stops the throw (and emits "after")
  • "f -write" and "f -throw" with/without -reset all keep throwing
function f
{
    param( [switch] $throw, [switch] $write, [switch] $reset )

    try {
        write-host EAP $ErrorActionPreference
        $originalEAP = $ErrorActionPreference
        $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

        if ($throw)
        {
            write-host throwing
            throw 'error'
        } elseif ($write) {
            write-host writeerror
            write-error 'error'
        } else {
            write-host invalidparameter
            Start-Transaction -foobar foo
        }

        write-host continued try
    }
    finally
    {
        if ($reset) {
            $ErrorActionPreference = $originalEAP
        }
        write-host in finally
    }

    write-host after
} 

Expected behavior

'''
PS G:\nt\src\onecore\sdktools\srvperf\vmfleet> f -reset
EAP Continue
invalidparameter
in finally
Start-Transaction:
Line |
  19 |              Start-Transaction -foobar foo
     |              ~~~~~~~~~~~~~~~~~
     | The term 'Start-Transaction' is not recognized as the name of a cmdlet, function, script file, or operable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
'''

Actual behavior

Note the 'after' emitted, showing that resetting the EAP affected the throwing error. Compare to behavior if using write-error/thrown error (-write and -throw respectively).

'''
PS G:\nt\src\onecore\sdktools\srvperf\vmfleet> f -reset
EAP Continue
invalidparameter
in finally
Start-Transaction:
Line |
  19 |              Start-Transaction -foobar foo
     |              ~~~~~~~~~~~~~~~~~
     | The term 'Start-Transaction' is not recognized as the name of a cmdlet, function, script file, or operable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
after
'''

Environment data

PS G:\nt\src\onecore\sdktools\srvperf\vmfleet> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      7.0.3
PSEdition                      Core
GitCommitId                    7.0.3
OS                             Microsoft Windows 10.0.19043
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. Dan Lovinger (dl2n)
    added
    Needs-TriageThe issue is new and needs to be triaged by a work group.The issue is new and needs to be triaged by a work group.
    on Jul 6, 2021
  2. jazzdelightsme commented on Jul 6, 2021

    @jazzdelightsme
    Dan Thompson (jazzdelightsme)
    on Jul 6, 2021
    Contributor
    More actions

    I concur that this behavior has been around for at least a decade.

    The problem does not repro if there is an Exception Handling (EH) frame in scope (above you on the stack). So, for example, if you write "try { f -reset } finally { }", then the problem will not repro. (You would think that addition of an empty finally block would have no effect, but it does: presence of the EH frame bypasses this bug.)

    For this reason (to work around this bug), I always write a "boiler plate" try { ... } finally { } in all my public/exported functions.

    Reacted by Kevin Marquette
  3. Ilya (iSazonov)
    added
    WG-Enginecore PowerShell engine, interpreter, and runtimecore PowerShell engine, interpreter, and runtime
    Issue-BugIssue has been identified as a bug in the productIssue has been identified as a bug in the product
    on Jul 7, 2021
  4. rjmholt commented on Jul 25, 2021

    @rjmholt
    Rob Holt (rjmholt)
    on Jul 25, 2021
    Collaborator
    More actions

    Copying from the original email thread to here to bring us closer to a fix:

    So I finally got a chance to look into this and I think it’s basically a deep and subtle semantics bug in PowerShell’s error handling (because errors in PowerShell are usually so simple…).

    First of all, the way PowerShell handles a series of statements is pretty involved; it sets up a state machine to execute each statement so that non-terminating errors can be resumed immediately. The problem is that the sequence of events for a statement that throws is as follows:

    • Execute the statement, until it throws
    • Jump to the catch block
    • Check the EAP
    • Depending on that, continue or stop

    If we imagine the statement that threw also sets the EAP in a finally block, we get this sequence:

    • Execute the statement until it throws
    • Set the EAP to the new value
    • Jump to the catch block
    • Check the EAP
    • Depending on that, continue or stop

    So the question is: why does this only happen for parameter binding exceptions?

    When a non-terminating error is promoted to a terminating one, it’s reified as an ActionPreferenceStopException which sets a property called SuppressPromptInInterpreter, which in turn marks the exception as a terminating one. Basically once an error has been made terminating, we mark it to stop checking the EAP when we see it and simply rethrow it up the callstack.

    However, a parameter binding exception is not promoted to an ActionPreferenceStopException, but instead is thrown as a ParameterBindingException. This bubbles up through a callstack that looks like this:

    st

    You can see there are a number of stack frames under System.Management.Automation.Interpreter, which is the very opaque PowerShell interpreter. Essentially, the parameter binding exception occurs under this compiler-generated call, which in a try/finally block is run by the EnterTryCatchFinallyInstruction. That thrown exception is then caught and handled by that initial strange code the compiler generated for the state machine (the actual codegen for that is here). That call ends up back in that check to see if the exception is terminating, but because try/catch blocks set $ExecutionContext.PropagateExceptionsToEnclosingStatementBlock to true the binding error is thrown before it can be made intrinsically terminating.

    So if we were to fix this, how we would do that? This is pretty complicated and the kind of place where a small change could have unexpected consequences (like the changes that were clearly made here for ErrorAction = Inquire, which I believe introduced this corner case bug). I think the existing catch-and-check-EAP behaviour is right, so the change should happen in the CheckActionPreference() method. The issue is that the ordering of the current checks is important because we might have the EAP changed by a trap or by EAP = Inquire. I think essentially we need to closely re-evaluate the logic there so that we:

    • Check for traps
    • Then check to see if the error must be thrown (including checking if the EAP is Stop)
    • Then check for inquiry
    • Then continue with other behaviours

    But all of this comes with the caveat that this is a long-standing corner case bug with a delicate fix, so correcting it might take some time and someone to champion it.

    Reacted by Patrick Meinecke
  5. rjmholt commented on Aug 5, 2021

    @rjmholt
    Rob Holt (rjmholt)
    on Aug 5, 2021
    Collaborator
    More actions

    /cc Bruce Payette (@bpayette) would be interested in your thoughts here

  6. daxian-dbw commented on Aug 18, 2021

    @daxian-dbw
    Dongbo Wang (daxian-dbw)
    on Aug 18, 2021
    Member
    More actions

    So the question is: why does this only happen for parameter binding exceptions?

    Not just parameter binding errors, but any general exceptions, such as a command-not-found error, exception thrown from an expression (e.g. 1/0), and exception thrown from a method invocation. For those error/exceptions, PowerShell treats them as non-terminating errors by default (with the default error action 'Continue'), unless they happen in a try block. When any of those errors/exceptions happens in a try block, it will be treated as a terminating error to that try block.

    ## behave like a non-terminating error
    PS> & { 1/0; Write-Host 'after' }
    RuntimeException: Attempted to divide by zero.
    after
    
    ## when wrapped in try block, behaves like a terminating error within the try block
    PS> & { try { 1/0; Write-Host 'in try' } finally { Write-Host 'in finally' }; Write-Host 'after' }
    in finally
    RuntimeException: Attempted to divide by zero.
    after
    
    ## of course, when error action is 'Stop', behaves like a terminating error to the whole script
    PS:1> $ErrorActionPreference = 'stop'
    PS:2> & { 1/0; Write-Host 'after' }
    ParentContainsErrorRecordException: Attempted to divide by zero.
    PS:3> & { try { 1/0; Write-Host 'in try' } finally { Write-Host 'in finally' }; Write-Host 'after' }
    in finally
    ParentContainsErrorRecordException: Attempted to divide by zero.

    You did a good summary of how PowerShell handles errors for a statement block:

    the sequence of events for a statement that throws is as follows:

    • Execute the statement, until it throws
    • Jump to the catch block
    • Check the EAP
    • Depending on that, continue or stop

    If we imagine the statement that threw also sets the EAP in a finally block, we get this sequence:

    • Execute the statement until it throws
    • Set the EAP to the new value
    • Jump to the catch block
    • Check the EAP
    • Depending on that, continue or stop

    Back to the repro, the try statement will throw the general exception (from here because ExecutionContext.PropagateExceptionsToEnclosingStatementBlock will be set to true within a try block), and the EAP is set back to 'Continue', so the steps in bold above will find the EAP is 'Continue' and thus treat this general exception as a non-terminating error.

    A similar behavior will happen to f -throw as well, if EAP is set to SilentlyContinue or Ignore in the finally block. This is because for throw statement, the exception behaves as terminating error in the default error action ('Continue'), but it respects the SilentlyContinue and Ignore preference.

    function f
    {
        param([switch] $reset )
        try {
            write-host EAP $ErrorActionPreference
            $originalEAP = $ErrorActionPreference
            $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
    
            write-host throwing
            throw 'error'
    
            write-host continued try
        }
        finally
        {
            if ($reset) {
                $ErrorActionPreference = 'SilentlyContinue'
            }
            write-host in finally
        }
    
        write-host after
    } 
    
    PS:1> f
    EAP Continue
    throwing
    in finally
    Exception:
    Line |
      10 |          throw 'error'
         |          ~~~~~~~~~~~~~
         | error
    
    PS:2> f -reset
    EAP Continue
    throwing
    in finally
    after

    The error handling in PowerShell has some subtle and not well known behaviors. We probably already see those behaviors a lot in other scenarios, but may never really notice the differences. I have summarized the error handling behaviors in the "Error Handling" section of the 'Clean' block RFC (PowerShell/PowerShell-RFC#294).

  7. daxian-dbw commented on Aug 18, 2021

    @daxian-dbw
    Dongbo Wang (daxian-dbw)
    on Aug 18, 2021
    Member
    More actions

    I'm not sure if we should call the reported behavior a bug.

    For the repro script, the code to restore $ErrorActionPreference doesn't need to be put in a finally block. The setting of $ErrorActionPreference doesn't affect the value of $ErrorActionPreference in the outer scope, so if the script execution gets interrupted by any terminating exception after setting EAP to 'stop', $ErrorActionPreference in the outer scope will still have the original value.

    So, given that, would the following change work for your scenario? Dan Lovinger (@dl2n)

    function f
    {
        param( [switch] $throw, [switch] $write, [switch] $reset )
    
        try {
            ## <same code in try>
            ....
        }
        finally
        {
            write-host in finally
        }
    
        ## Move this out from the finally block
        if ($reset) {
            $ErrorActionPreference = $originalEAP
        }
    
        write-host after
    } 
  8. rjmholt commented on Aug 18, 2021

    @rjmholt
    Rob Holt (rjmholt)
    on Aug 18, 2021
    Collaborator
    More actions

    I'm not sure if we should call the reported behavior a bug... $ErrorActionPreference doesn't need to be put in a finally block

    Yeah, I see your point there that the finally is unneeded. But, to me this is a question of error semantics in PowerShell. When you throw an error, should it be overridable in some way by a state change afterward? Should it be different for different errors?

    I think the answer to the second part is pretty clearly no. But for the first part it's actually not so simple. Consider this:

    function f
    {
        try
        {
            return 1
        }
        finally
        {
            return 2
        }
    }

    It turns out this is a parse error in PowerShell... But in Python it's not:

    def f():
        try:
            return 1
        finally:
            return 2
    
    f() # returns 2

    And equivalently in PowerShell:

    function f
    {
        try
        {
            throw 'bad'
        }
        finally
        {
            throw 'worse'
        }
    }
    
    f     # throws 'worse'

    So we see here that a value that should leave the function in the try block can be altered in the finally block; the finally block truly is final. The question in my mind is: if we throw an error because the EAP was one value, then post-throw alter that value, should it affect the error we're still in the process of throwing?

  9. daxian-dbw commented on Aug 18, 2021

    @daxian-dbw
    Dongbo Wang (daxian-dbw)
    on Aug 18, 2021
    Member
    More actions

    should it be overridable in some way by a state change afterward

    It was not overridden. The check on EAP 'stop' wasn't even hit in CheckErrorActionPreference method for the throw case and the general exception case from the repro script. The EAP was hit in the Write-Error case, and as you observed, it cannot be overridden afterwards.

    Once the check on EAP 'stop' is hit in CheckErrorActionPreference, the exception becomes a terminating error to the whole script and it cannot be overridden.

    Should it be different for different errors?

    I'm sure there will be different opinions on the current error handling behaviors, especially the subtle ones. But it has been working in this way for a long time, and any change to the current behavior would unfortunately be breaking.

    It turns out this is a parse error in PowerShell... But in Python it's not:

    It's a compiling error in C# as well.

    And equivalently in PowerShell:

    That's the same behavior in C#.

    So we see here that a value that should leave the function in the try block can be altered in the finally block; the finally block truly is final.

    Are you still referring to the exception overridden example from the finally block? PowerShell is using the same behavior as in C#.

    The question in my mind is: if we throw an error because the EAP was one value, then post-throw alter that value, should it affect the error we're still in the process of throwing?

    Again, the thing is in the repro script, throw and the general exception were not thrown because of EAP is 'stop' (they will throw even EAP is the default 'Continue'). The check on EAP 'stop' wasn't hit in those 2 cases until the execution leaves the try statement (where the EAP was already changed to 'Continue'). Once it's hit, the exception cannot be altered anymore -- it will be a terminating error to the whole script.


    Just to add a bit more clarification:
    The throw and the general exception were re-thrown from here because ExecutionContext.PropagateExceptionsToEnclosingStatementBlock gets set to true within a try block.

  10. dl2n commented on Aug 19, 2021

    @dl2n
    Dan Lovinger (dl2n)
    on Aug 19, 2021 via email
    Author
    More actions
    This observation is correct – the change to EAP does not need to be in the finally clause other than as a matter of semantic/cosmetic grouping. The problem is the subtle landmine of code location which logically does not seem that it should matter at all. What is the overriding documentable behavior? That’s what’s most painful about this.
  11. Dongbo Wang (daxian-dbw)
    self-assigned this
    on Jan 6, 2022
  12. Dongbo Wang (daxian-dbw)
    removed their assignment
    on Sep 18, 2023
  13. Patrick Meinecke (SeeminglyScience)
    self-assigned this
    on Sep 18, 2023
  14. SeeminglyScience commented on Sep 19, 2023

    @SeeminglyScience
    Patrick Meinecke (SeeminglyScience)
    on Sep 19, 2023
    Contributor
    More actions

    @PowerShell/wg-powershell-engine discussed this yesterday and came to a lot of the same conclusions as have already been shared in this thread. Our current thought is that attempting to fix this edge case is very risky, so for now we're going to consider this wontfix. I'm not actually sure if it's possible to fix this without causing new bugs.

    I did want to include a C# representation of the generated code (created with ScriptBlockDisassembler)

    Generator

    {
        try {
            $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
            throw
        } finally {
            $ErrorActionPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue
        }
    
    } | ScriptBlockDisassembler\Get-ScriptBlockDisassembly -Minimal

    Result

    (with added comments)

    static class GeneratedCode
    {
        static void EndBlock(FunctionContext funcContext)
        {
            try
            {
                ExecutionContext context = funcContext._executionContext;
                var locals = funcContext._localsTuple;
                try
                {
                    bool oldActiveHandler0 = context.PropagateExceptionsToEnclosingStatementBlock;
                    context.PropagateExceptionsToEnclosingStatementBlock = true;
                    int stmt = 0;
    
                    unnamed_label_0:
                    try
                    {
                        switch (stmt)
                        {
                            case 0:
                                goto unnamed_label_1;
                                break;
    
                            case 1:
                                goto unnamed_label_2;
                                break;
    
                            case 2:
                                goto unnamed_label_3;
                                break;
                        }
    
                        // Try block start
                        unnamed_label_1:
                        stmt = 1;
                        // $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
                        locals.Item009 = ActionPreference.Stop;
    
                        unnamed_label_2:
                        stmt = 2;
                        // throw
                        throw ExceptionHandlingOps.ConvertToException(null, Fake.Const<InternalScriptExtent>("throw"), false);
    
                        // Try block end
                    }
                    catch (FlowControlException)
                    {
                        throw;
                    }
                    catch (Exception exception)
                    {
                        // Exception is rethrown here because error action preference
                        // is current `Stop`
                        ExceptionHandlingOps.CheckActionPreference(funcContext, exception);
                        goto unnamed_label_0;
                    }
    
                    unnamed_label_3:
                }
                finally
                {
                    context.PropagateExceptionsToEnclosingStatementBlock = oldActiveHandler0;
                    bool oldIsStopping1 = ExceptionHandlingOps.SuspendStoppingPipeline(context);
                    try
                    {
                        try
                        {
                            // Finally block start
                            // $ErrorActionPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue
                            locals.Item009 = ActionPreference.SilentlyContinue;
    
                            // Finally block end
                        }
                        catch (FlowControlException)
                        {
                            throw;
                        }
                        catch (Exception exception)
                        {
                            // This catch is *not* hit
                            ExceptionHandlingOps.CheckActionPreference(funcContext, exception);
                        }
                    }
                    finally
                    {
                        ExceptionHandlingOps.RestoreStoppingPipeline(context, oldIsStopping1);
                    }
                }
            }
            catch (FlowControlException)
            {
                throw;
            }
            catch (Exception exception)
            {
                // Exception from try block propagates here, but now `ErrorActionPreference`
                // is `SilentlyContinue`, so it's ignored.
                ExceptionHandlingOps.CheckActionPreference(funcContext, exception);
            }
        }
    }
  15. Patrick Meinecke (SeeminglyScience)
    removed their assignment
    on Sep 19, 2023
  16. Patrick Meinecke (SeeminglyScience)
    added
    Resolution-Won't FixThe issue won't be fixed, possibly due to compatibility reason.The issue won't be fixed, possibly due to compatibility reason.
    and removed
    Needs-TriageThe issue is new and needs to be triaged by a work group.The issue is new and needs to be triaged by a work group.
    on Sep 19, 2023
  17. microsoft-github-policy-service commented on Sep 21, 2023

    @microsoft-github-policy-service
    microsoft-github-policy-serviceBot
    on Sep 21, 2023
    Contributor
    More actions

    This issue has been marked as won't fix and has not had any activity for 1 day. It has been closed for housekeeping purposes.

  18. microsoft-github-policy-service
    closed this as completedon Sep 21, 2023
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

    Issue-BugIssue has been identified as a bug in the productIssue has been identified as a bug in the productResolution-Won't FixThe issue won't be fixed, possibly due to compatibility reason.The issue won't be fixed, possibly due to compatibility reason.WG-Enginecore PowerShell engine, interpreter, and runtimecore 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

      You are visitor 01402520 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.