Roman Kuzmin
Roman Kuzmin

Reputation: 42033

PowerShell ThrowTerminatingError() errors are not true terminating errors

This is either a bug report prelude or a question about the design logic and usefulness. And in any case, a question on how to develop cmdlets with true terminating errors.

In a clean session, i.e. with $ErrorActionPreference = 'Continue' and without try or trap, invoke any of the following commands (or even all together):

# script module, missing path
Save-Module InvokeBuild -Path zzz -ErrorAction Stop; 'Continued!'

# cmdlet, invalid data
ConvertFrom-Json zzz -ErrorAction Stop; 'Continued!'

# cmdlet, invalid command syntax
Get-Module -Name zzz -FullyQualifiedName zzz -ErrorAction Stop; 'Continued!'
Export-Csv -Path zzz -LiteralPath zzz -InputObject 1 -ErrorAction Stop; 'Continued!'

As a result, 'Continued!' is invoked after each command that uses ThrowTerminatingError(). It turns out this method terminates the command itself but not the calling script.

These are just a few contrived examples, there are many others. Examples are not real but the issue bites for real sometimes.

Note that -ErrorAction Stop does not help, it is not designed for such errors. It affects non-terminating errors (WriteError(), Write-Error).


And the practical question: is there a way to emit a terminating error which is terminating for the current command and the calling script?

It looks like in scripts this will do (in some cases, not all):

Write-Error -ErrorAction Stop "message"

Note that throw "message" is really terminating. But there is important difference from Write-Error and ThrowTerminatingError(). Its error location points to the line of throw. This is not always as useful as the line with the failing command invocation, especially on syntax or input issues, i.e. terminating issues by nature.

So scripts have something. But what can cmdlets do? Available WriteError() is not terminating. Raw throw is not much different from ThrowTerminatingError().


UPDATE

Indeed, $ErrorActionPreference = 'Stop' makes ThrowTerminatingError() terminating in the calling script. So do try and trap.

This is not what I am looking for. I specifically noted $ErrorActionPreference = 'Continue', the default. I am looking for absolute terminating errors regardless if the default settings. Compare, in scripts throw is always terminating (though it is not perfect, as mentioned). I am looking for the analogue in .NET cmdlets.

Users forget to set $ErrorActionPreference = 'Stop'. I forget this sometimes. And some cases like invalid commands should stop the calling script regardless of settings. At least command authors should be able to choose this option.

Upvotes: 1

Views: 1400

Answers (2)

user4003407
user4003407

Reputation: 22122

You can throw the same exception as throw statement in PowerShell throw:

Add-Type -TypeDefinition @‘
    using System;
    using System.Management.Automation;
    [Cmdlet(VerbsDiagnostic.Test, "Throw")]
    public class TestThrow : Cmdlet {
        protected override void ProcessRecord() {
            throw new RuntimeException("Message") { WasThrownFromThrowStatement=true };
        }
    }
’@ -PassThru|Select-Object -First 1 -ExpandProperty Assembly|Import-Module

Test-Throw; 'Not printed'

In scripts it can be done in this way (requires calling internal method):

function Test-ScriptThrow {
    $ErrorRecordType = [System.Management.Automation.ErrorRecord]
    $ErrorRecord = $ErrorRecordType::new([Exception]::new("Message"), 'ErrorId', 'NotSpecified', $null)
    $ErrorRecordType.InvokeMember('SetInvocationInfo', 'Instance, NonPublic, InvokeMethod', $null, $ErrorRecord, $MyInvocation)
    throw $ErrorRecord
}

Test-ScriptThrow; 'Not printed'

It seems that same thing is necessary for C# cmdlet:

Add-Type -TypeDefinition @‘
    using System;
    using System.Reflection;
    using System.Management.Automation;
    [Cmdlet(VerbsDiagnostic.Test, "Throw2")]
    public class TestThrow2 : PSCmdlet {
        protected override void ProcessRecord() {
            ErrorRecord errorRecord = new ErrorRecord(new Exception("Message"), "ErrorId", ErrorCategory.NotSpecified, null);
            typeof(ErrorRecord).InvokeMember("SetInvocationInfo", BindingFlags.Instance|BindingFlags.NonPublic|BindingFlags.InvokeMethod, null, errorRecord, new[] { MyInvocation });
            throw new RuntimeException(null, null, errorRecord) { WasThrownFromThrowStatement=true };
        }
    }
’@ -PassThru|Select-Object -First 1 -ExpandProperty Assembly|Import-Module

Test-Throw2 | % { Some other command }; 'Not printed'

Otherwise it underline whole statement as error, not only one command:

Test-Throw | % { Some other command }; 'Not printed'

Upvotes: 2

Mathias R. Jessen
Mathias R. Jessen

Reputation: 174555

As a result, 'Continued!' is invoked after each command that uses ThrowTerminatingError(). It turns out this method terminates the command itself but not the calling script.

Of course not - your $ErrorActionPreference is set to Continue - so when a cmdlet invocation throws an error, the repl (or script) simply resumes (continues) execution.

$ErrorActionPreference is local to the scope in which it is defined, so you can set it inside a function or script to override the user's preference (but only within the scope of the function/script):

script.ps1

[CmdletBinding()]
param()

$ErrorActionPreference = 'Stop'

# script module, missing path
Save-Module InvokeBuild -Path zzz -ErrorAction Stop; 'Continued!'

# cmdlet, invalid data
ConvertFrom-Json zzz -ErrorAction Stop; 'Continued!'

# cmdlet, invalid command syntax
Get-Module -Name zzz -FullyQualifiedName zzz -ErrorAction Stop; 'Continued!'
Export-Csv -Path zzz -LiteralPath zzz -InputObject 1 -ErrorAction Stop; 'Continued!'

It'll return immediately after the error from Save-Module (even if $ErrorActionPreference is set to Continue in the calling scope)

Upvotes: 0

Related Questions