Powershell: How, exactly, does PowerShell decide whether .ThrowTerminatingError() terminates only the statement?

Created on 3 Feb 2018  路  10Comments  路  Source: PowerShell/PowerShell

Consider the following code:

function a {
    param ( [Parameter(ValueFromPipeline)]$x )
    process {
        $PSCmdlet.ThrowTerminatingError(
            [System.Management.Automation.ErrorRecord]::new(
                'exception message',
                'errorId',
                [System.Management.Automation.ErrorCategory]::InvalidOperation,
                $null
            )
        )
    }
}

Invoking

a
Write-Host 'statement after'

outputs

a : exception message
At C:\test1.ps1:15 char:1
+ a
+ ~
+ CategoryInfo          : InvalidOperation: (:) [a], Exception
+ FullyQualifiedErrorId : errorId,a

statement after

which seems to be consistent with .ThrowTerminatingError() resulting in a "statement-terminating error".

On the other hand, invoking

try
{
    a
    Write-Host 'statement after'
}
catch
{
    Write-Host 'catch'
}

outputs catch which indicates that, in this case, .ThrowTerminatingError() terminates more than just the statement.

What is happening with flow of control in the code with the try{} block? Does PowerShell search the whole call stack for a try{}catch{}? Are there circumstances aside from a wrapping try{} that results in .ThrowTerminatingError terminating more than just the statement?

FWIW, this arose trying to understand PowerShell/PowerShell-Docs#1583.

Issue-Question Resolution-Answered

All 10 comments

I believe we terminating pipeline https://github.com/PowerShell/PowerShell/issues/2860#issuecomment-294914902

I believe we terminating pipeline #2860 (comment)

If it were that simple, then the output of

try
{
    1 | a
    Write-Host 'statement after'
}
catch
{
    Write-Host 'catch'
}

would be statement after not catch. The output is catch indicating that more happened with flow of control than merely terminating the pipeline established by 1 | a.

You should view this from the perspective of the try / catch statement:

If a _terminating_ error occurs - whether script- or statement-terminating - the _entire try block_ is exited _at the point the error occurs_ and execution resumes in the catch block.

(A _nonterminating_ error, by contrast, is _not_ caught.)

Except for the terminating-vs.-nonterminating error distinction, this is consistent with the usual try / catch semantics in, say, C#.

@mklement0

Just to make sure we're on the same page, the situation is at least this complicated:

| Error Reported by | inside try{} | no try {} |
|-------------------|--------------|-----------|
|Write-Error | continue with next statement | continue with next statement |
|throw | jump to catch{} | stop execution |
|$PSCmdlet.ThrowTerminatingError | jump to catch{} | continue with next statement |

It seems to me that $PSCmdlet.ThrowTerminating() is neither consistently "non-terminating" nor consistently "terminating".

Except for the terminating-vs.-nonterminating error distinction, this is consistent with the usual try / catch semantics in, say, C#.

I'm not proficient at C#. Are there C# statements that continue to the next statement if there is no surrounding try{} block but jump to catch{} otherwise? That's what $PSCmdlet.ThrowTerminating() seems to do. I would consider PowerShell throw's behavior consistent with usual try/catch semantics, but not $PSCmdlet.ThrowTerminating().

If a terminating error occurs - whether script- or statement-terminating - the entire try block is exited at the point the error occurs and execution resumes in the catch block.

This much is evident from the repro in my OP. But it doesn't really answer these questions from my OP:

Does PowerShell search the whole call stack for a try{}catch{}? Are there circumstances aside from a wrapping try{} that results in .ThrowTerminatingError terminating more than just the statement?

So far, I don't see anything that contradicts my explanation.

It seems to me that $PSCmdlet.ThrowTerminating() is neither consistently "non-terminating" nor consistently "terminating".

_Nothing_ is - by design - _consistently_ terminating vs. nonterminating in PowerShell:

The point of the $ErrorActionPreference preference variable / -ErrorAction parameter is to _control_ the "terminatingness" of the behavior _as needed_ (albeit in an inconsistent manner, and inconsistent with the documentation, as discussed in the linked docs issue).

Does PowerShell search the whole call stack for a try{}catch{}?

From what I can tell, yes.

Try this simple example (all scripts are assumed to reside in the current location):

Script t2.ps1:

Get-Item -NoSuchParam   # provoke statement-terminating error
"With the defaults, I'm pretty sure I won't execute, due to the try / catch on the call stack"

Script t1.ps1:

try {
  ./t2.ps1
  "What are you doing here?"
} catch {
  'ouch'
}

Running ./t1.ps yields:

ouch

That is, the statement-terminating error inside ./t2.ps1 aborted both the t2.ps - due to the try / catch on the call stack - and, bubbling up, the the try block, ultimately executing only the catch block in t1.ps1.

Are there circumstances aside from a wrapping try{} that results in .ThrowTerminatingError terminating more than just the statement?

Not that I'm aware of, but do let us know if you find such cases.

Nothing is - by design - consistently terminating vs. nonterminating in PowerShell:

This might be down to semantics, but throw consistently terminates in my experience.

Try this simple example...

The trouble with that approach to answering this question is that it proves what happens in that particular set of circumstances and not much else. I can get a more applicable version of that kind of information from unit testing commands that use $PSCmdlet.ThrowTerminatingError(). But that information doesn't tell me where to expect surprises the way an analytical answer often does. It seems like an analytical answer to this question is possible.

This might be down to semantics, but throw consistently terminates in my experience.

  • Except if you use try / catch (so, yes, _by default_ throw is _runspace-terminating_).

  • Yes, throw is the "apex error" (what I call a _script-terminating error_ / runspace-terminating error) - it _directly_ throws a _by default uncaught_ exception that terminates the runspace.

  • You can _promote_ other types of errors (nonterminating / statement-terminating) to "apex errors" via $ErrorActionPreference = 'Stop' / -ErrorAction Stop.

  • In the end, you can catch them with try / catch , anywhere up the call stack.

    • If they're already _statement_-terminating errors, however, you needn't promote them - try / catch will catch them as-is (just as it catches _script_-terminating errors, but _unlike_ _nonterminating_ errors).
  • If you don't, they terminate the runspace.

If you find a scenario that doesn't fit this model (which is based on _my_ experiments), let us know.

The trouble with that approach to answering this question is that it proves what happens in that particular set of circumstances and not much else. I can get a more applicable version of that kind of information from unit testing commands that use $PSCmdlet.ThrowTerminatingError(). But that information doesn't tell me where to expect surprises the way an analytical answer often does. It seems like an analytical answer to this question is possible.

I don't know what you mean.

If you find a scenario that doesn't fit this model (which is based on my experiments), let us know.

You model _seems_ to fit. I think you almost have me convinced. :)

Thanks for your help @mklement0!

BrucePay wrote in PowerShell/PowerShell#6286(comment):

Note that the automatic change from statement-terminating error to exception in the presence of trap or try/catch is a fundamental semantic for PowerShell.

(Replacing my earlier comment; got confused)

To think of this in terms of _exceptions_ is a bit confusing, because the use of exceptions is an _implementation detail_, given that PowerShell's native "error currency" is error records ([System.Management.Automation.ErrorRecord] instances).

  • Both trap and try / catch allow you to _intercept_ statement-terminating errors _and_ script-terminating errors - which in the presence of these constructs are _ignored_ by default, unless you take _explicit action_ : to truly generate a script-terminating error (unhandled exception), you must, in a trap block, explicitly execute break, and, in a catch block, explicitly execute Throw.

  • By contrast, using $ErrorActionPreference = 'Stop' is the only way to _directly_ promote _both_ nonterminating and statement-terminating errors _directly_ to script-terminating errors (unhandled exceptions) (the equivalent of using Throw).
    While you can _also_ catch those with trap and try / catch, in their absence such promoted errors will _terminate the script_.

    • In other words: $ErrorActionPreference = 'Stop' doesn't just promote nonterminating errors to statement-terminating errors, it promotes _both_ types to _script_-terminating errors.
Was this page helpful?
0 / 5 - 0 ratings