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 #15726DescriptionPrerequisites
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 Activityjazzdelightsme commented on Jul 6, 2021 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 rjmholt commented on Jul 25, 2021 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:

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 rjmholt commented on Aug 5, 2021 daxian-dbw commented on Aug 18, 2021
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). daxian-dbw commented on Aug 18, 2021 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
} rjmholt commented on Aug 18, 2021
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? daxian-dbw commented on Aug 18, 2021
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. dl2n commented on Aug 19, 2021 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. SeeminglyScience commented on Sep 19, 2023 @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);
}
}
}microsoft-github-policy-service commented on Sep 21, 2023 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.
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|