Powershell: Is there a way to distinguish between caught script- and statement-terminating errors?

Created on 1 Mar 2018  路  6Comments  路  Source: PowerShell/PowerShell

I've been experimenting with an improved test fixture for PowerShell commands. I am hoping to find a way to distinguish between script- and statement-terminating errors (using the terminology from PowerShell/PowerShell-Docs#1583). A (very) simplified version of the core of the test fixture looks something like this:

function TestFixture
{
    param
    (
        [System.Management.Automation.CommandInfo]
        $CommandUnderTest
        # ...
    )
    # ...
    try
    {
        # ...
        UpstreamFixturePart $InputObject |
            & $CommandUnderTest @NamedArgs @ArgumentList |
            DownstreamFixture
        # ...
    }
    catch
    {
        # ...
    }
}

The obvious way to distinguish between a script- and statement-terminating error is to test control flow behavior when the error occurs (see #6098 for an example). The try {} around $CommandUnderTest in TestFixture is necessary to capture and record exceptions as they are thrown, and keep flow of control within the test fixture. Unfortunately, the presence of that try{} block precludes the using control flow behavior to distinguish script- from statement-terminating errors because both kinds of errors result in the same control flow (ie. both jump to the catch block).

I was hoping that .WasThrownByThrowStatement would provide enough information to distinguish between the two. Unfortunately per #6288 and #3647 (comment) that is not currently sufficient to distinguish.

  1. Is there a way to distinguish between script- and statement-terminating errors once they are caught in the scriptblock?
  2. Is there a way to preserve the distinction in control flow behavior between a statement- and script-terminating errors _and_ retain control of flow in the fixture?
  3. Is there some other way to distinguish between script- and statement-terminating errors in this scenario?
WG-Engine

Most helpful comment

@BrucePay

If -ErrorAction Stop is specified, that non-terminating write becomes a statement-terminating error.

In my experimentation Write-Error -ea Stop, for example, does not cause a statement-terminating error.
Rather, Write-Error -ea Stop seems to always throw an exception. In my experimentation the effect of -ErrorAction has been consistent with being an implementation detail of the command to which it is passed. Do you mean $ErrorActionPreference = 'Stop' here?

In this scenario, the same error object eventually becomes all three "types" as it flows through the system - it began as non-terminating, became terminating and then became an exception.

I think I understand the escalation concept you are describing. I have not, however, come across any one error source that observably escalates from non-terminating to statement-terminating to exception in the idealized way you describe. Do you have an example of a command that observably behaves the way you describe? Or are you describing an unobservable internal escalation process here?


I arrived at the following table empirically (test code here).

|ref.| Statement | In try{}? | ErrorActionPreference | Exception Thrown by statement? |
|---|-------------|---------------------------|-----------|---------------|
| 1 |throw | x | not SilentlyContinue | yes |
| 2 |throw | yes | SilentlyContinue | yes |
| 3 |throw | no | SilentlyContinue | no |
| 4 |ThrowTerminatingError() | yes | x | yes |
| 5 |ThrowTerminatingError() | no | Stop| yes |
| 6 |ThrowTerminatingError() | no | Continue | no |
| 7 |Write-Error -ea Stop | x | x | yes |
| 8 |Write-Error | x | Stop | yes |
| 9 |Write-Error | x | Continue | no |

x = does not matter

Another way to ask my original question is as follows:

Is there a way to distinguish between a caught exception thrown by throw (ie. ref. 1 and 2) and an exception thrown by ThrowTerminatingError() (ie. ref. 4)?

In yet other words, is there a way to infer from a caught exception whether that exception would have been thrown in the absence of the try{}catch{} that caught it?

All 6 comments

Related #4837, #4781
~Dup #3158~

@iSazonov This issue is not a dup of #3158:

  • #3158 is about distinguishing between terminating and non-terminating errors recorded in $Error
  • this issue is about distinguish between two kinds of terminating errors, script- and statement-terminating errors in a catch block

@mklement0 If you have the time, could you take a look at this and see if you have any ideas for how to achieve this?

Hi @alx9r, There aren't really three kinds of errors as such, just three dispositions of one kind of error. For example, if a command writes an error to error output, it's non-terminating. If -ErrorAction Stop is specified, that non-terminating write becomes a statement-terminating error. If there is a trap or try/catch in scope, the statement-terminating error is thrown to the catch block. In this scenario, the same error object eventually becomes all three "types" as it flows through the system - it began as non-terminating, became terminating and then became an exception. (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.)

@BrucePay

If -ErrorAction Stop is specified, that non-terminating write becomes a statement-terminating error.

In my experimentation Write-Error -ea Stop, for example, does not cause a statement-terminating error.
Rather, Write-Error -ea Stop seems to always throw an exception. In my experimentation the effect of -ErrorAction has been consistent with being an implementation detail of the command to which it is passed. Do you mean $ErrorActionPreference = 'Stop' here?

In this scenario, the same error object eventually becomes all three "types" as it flows through the system - it began as non-terminating, became terminating and then became an exception.

I think I understand the escalation concept you are describing. I have not, however, come across any one error source that observably escalates from non-terminating to statement-terminating to exception in the idealized way you describe. Do you have an example of a command that observably behaves the way you describe? Or are you describing an unobservable internal escalation process here?


I arrived at the following table empirically (test code here).

|ref.| Statement | In try{}? | ErrorActionPreference | Exception Thrown by statement? |
|---|-------------|---------------------------|-----------|---------------|
| 1 |throw | x | not SilentlyContinue | yes |
| 2 |throw | yes | SilentlyContinue | yes |
| 3 |throw | no | SilentlyContinue | no |
| 4 |ThrowTerminatingError() | yes | x | yes |
| 5 |ThrowTerminatingError() | no | Stop| yes |
| 6 |ThrowTerminatingError() | no | Continue | no |
| 7 |Write-Error -ea Stop | x | x | yes |
| 8 |Write-Error | x | Stop | yes |
| 9 |Write-Error | x | Continue | no |

x = does not matter

Another way to ask my original question is as follows:

Is there a way to distinguish between a caught exception thrown by throw (ie. ref. 1 and 2) and an exception thrown by ThrowTerminatingError() (ie. ref. 4)?

In yet other words, is there a way to infer from a caught exception whether that exception would have been thrown in the absence of the try{}catch{} that caught it?

@alx9r: Nice writeup; to add to the general part:

Indeed, to recap from the error-handling saga:

  • _Preference variable_ $ErrorActionPreference = 'Stop' makes _both_ nonterminating and statement-terminating errors throw an unhandled-by-default exception (for which I coined the term _script-terminating_ error, which I'll use in the remainder of this post).

  • _Common parameter_ -ErrorAction Stop, by contrast, only affects _nonterminating_ errors and promotes them _directly to script-terminating_ errors.

This directly contradicts the docs, which do not distinguish between preference-variable and common-parameter use and categorically claim that Stop only affects _nonterminating_ errors (which is only true with the _common parameter_).

You can't help but wonder if the original intent was:

  • to never affect statement-terminating errors with _preference variable_ $ErrorActionPreference = 'Stop', in line with the _common parameter_'s behavior and the documentation.

  • to have both $ErrorActionPreference = 'Stop' and -ErrorAction Stop promote _nonterminating_ errors to _statement_-terminating ones rather than to script-terminating ones.

    • This would explain why -ErrorAction Stop has no effect on statement-terminating errors: they already _are_ statement-terminating errors; in other words: they already are in the desired target state.

In other words: the original intent may have been to _never_ terminate the script _by default_ , except with the use of Throw, and to instead _require_ use of try/catch or trap in order to effect termination.

  • The Throw documentation confusingly talks about generating a "terminating error" too, without pointing out the fundamental distinction from a _statement_-terminating error.
  • Additionally, the documentation's example use case shows use of Throw to enforce a mandatory parameter without prompting; of course, using this technique results in a _script_-terminating error, whereas cmdlets normally only generate _statement_-terminating errors, even in the face of incorrect syntax and, if the PowerShell instance was invoked with -NonInteractive, in the absence of mandatory parameters (declared without Throw).

@BrucePay, can you shed light on this?

Was this page helpful?
0 / 5 - 0 ratings