Search code examples
powershellerror-handlingstandards

PowerShell ErrorRecord.CategoryInfo.Activity (CategoryActivity)


So PowerShell error reporting seems like a huge mess considering that there are four* ways to report an error (not including the effects of $ErrorActionPreference, try/catch, and trap blocks which have the potential to alter how an error is reported by another source), with minute and poorly documented differences, and even worse there's no real instruction on when to use the different types. (I realize it varies on the situation, but what's the "standard" - what will others be expecting me to do?)

That's not really the main point here though. I've decided to go with the simplest option when possible as that's what I expect most others will be using, so at least we'll have consistency - but I'm either not understanding something properly or it's broken.

According to the PowerShell documentation, the Activity property of ErrorCategoryInfo class (observed in the CategoryInfo property of System.Management.Automation.ErrorRecord) should be a "text description of the operation which encountered the error". Considering the fact that the value is settable and (in the case of compiled cmdlets) indicates the name of the cmdlet unless otherwise specified, I would expect the same functionality in script-based error reporting methods, but I'm doing something wrong or I've found a bug in PowerShell that has pervaded since PowerShell 1.0.

Consider the following example:

using namespace System.Management.Automation;
function Invoke-Error1 {
    [CmdletBinding()]
    param() 
    process {
        $ex = [InvalidOperationException]::new()
        $er = [ErrorRecord]::new($ex, 'OperationTest', [ErrorCategory]::InvalidOperation, 'MyErrorTarget')
        $er.CategoryInfo.Activity = 'MyActivity'
        Write-Error -ErrorRecord $er
    }
}

Invoking this function will result in the following error.

invoke-error1 : Operation is not valid due to the current state of the object.
At line:1 char:1
+ invoke-error1
+ ~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (MyErrorTarget:String) [Write-Error], InvalidOperationException
    + FullyQualifiedErrorId : OperationTest,Invoke-Error1

When investigating further, you'll notice that the CategoryInfo.Activity property has been replaced by Write-Error. The value I set ("MyActivity") is ignored. If this were expected, I wouldn't expect a parameter on the Write-Error method to set the error category - and yet one seems to exist. (See Get-Help Write-Error -Parameter CategoryActivity. I can't see anything else that this might be intended to do.)

This can be confirmed through the following function.

function Invoke-Error2 {
    [CmdletBinding()]
    param() 
    process {
        $Params = @{
            Exception         = [InvalidOperationException]::new()
            ErrorId           = 'OperationTest'
            Category          = [ErrorCategory]::InvalidOperation
            TargetObject      = 'MyErrorTarget'
            CategoryActivity  = 'MyActivity'
        }
        Write-Error @Params
    }
}

The error reported is identical in all ways to the error from Invoke-Error1 aside from the name of the cmdlet and associated InvocationInfo. Specifically, note again that $Error[0].CategoryInfo.Activity -eq 'Write-Error' instead of "MyActivity".

And yet, there is a way to write an error that does properly write the error activity with the error... but it seems to be the lowest on the list of recommended ways to report an error since it's generally the same as Write-Error.

function Invoke-Error3 {
    [CmdletBinding()]
    param()
    process {
    $ex = [InvalidOperationException]::new()
        $er = [ErrorRecord]::new($ex, 'OperationTest', [ErrorCategory]::InvalidOperation, 'MyErrorTarget')
        $er.CategoryInfo.Activity = 'MyActivity'
        $PSCmdlet.WriteError($er)
    }
}

This function is identical in all ways to Invoke-Error1 except that I use $PSCmdlet.WriteError instead of Write-Error. According to certain community standards, it's preferred to use built-in functions over .NET calls, though of course when behavior differs it makes sense to use the procedure that produces the desired result.

This is the most similar method to how any compiled cmdlet writes an error. As a result, any error written by a cmdlet will use either a manually set Activity value, or use the default of the cmdlet's name. I believe the latter of these is somehow what's happening with Write-Error.

That being said, I can't feel like I'm missing something here. The CategoryInfo property and associated CategoryActivity parameter of Write-Error have existed since PowerShell 1.0. Certainly if this functionality never worked someone would have noticed by now? So what am I doing wrong? How would I properly report the error? Going back to the purpose of the Activity property of ErrorCategoryInfo, "Write-Error" is not the operation that encountered the error - my function did, and I want to report it using Write-Error. Is this possible?


*If you aren't aware, the errors that can be reported from a PowerShell script or script function are the following:

  • Script-Terminating (via throw)
  • Statement-Terminating (via $PSCmdlet.ThrowTerminatingError)
  • Non-Terminating Error (via Write-Error)
  • Non-Terminating Error (via $PSCmdlet.WriteError). This is much harder to distinguish from Write-Error but does behave differently - note this question, and the fact that $ErrorActionPreference does not affect the behavior of an error emitted directly by $PSCmdlet.WriteError (though $ErrorActionPreference will affect behavior of an error emitted indirectly from $PSCmdlet.WriteError if the direct source is a script). This can be tested by setting $ErrorActionPreference inside your function that proceeds to call $PSCmdlet.WriteError vs Write-Error.
  • In addition to these methods, any non-terminating error can be turned into a statement-terminating error via $ErrorActionPreference or the -ErrorAction common parameter. The $? variable can be used in error reporting but seems like a terrible way to do so based on how much the result of that variable varies. try/catch blocks can be used to catch errors and access them through the $PSItem variable, but within that block if you're executing from a function defined in a module you may or may not see the error through $Error depending on whether or not the command that reported the error is defined in your module. Writing a permeation of a caught exception may yield two errors being added to the $global:Error variable - the caught exception that I'd expect to be ignored will be placed in $global:Error if the command that caught it is defined in a module other than the one the error was thrown from, and the new error that I reported.

Solution

  • First, kudos for your in-depth analysis.

    You've already found an effective workaround that also works in Windows PowerShell for having your custom .Activity value honored: use of $PSCmdlet.WriteError() - though note that $PSCmdlet is only available in advanced functions and scripts.

    That an .Activity value isn't honored if it is part of a System.Management.Automation.ErrorRecord instance passed in full to Write-Error's -ErrorRecord parameter, whereas it now is if you use the -CategoryActivity parameter (shorter alias: -Activity) in PowerShell (Core)7+, is arguably a bug that I encourage you to report at the PowerShell GitHub repo.

    Note: As you point out, the following alternative workarounds are effective in PowerShell (Core) 7+ only:

    A slightly less obscure workaround that doesn't require $PSCmdlet and is therefore also available in non-advanced functions and scripts:

    using namespace System.Management.Automation
    
    $ex = [InvalidOperationException]::new()
    $er = [ErrorRecord]::new($ex, 'OperationTest', [ErrorCategory]::InvalidOperation, 'MyErrorTarget')
    $er.CategoryInfo.Activity = 'MyActivity'
    
    # Workaround: Specify the activity *also* via -Activity
    Write-Error -ErrorRecord $er -Activity $er.CategoryInfo.Activity
    

    Note: To see a friendly representation of the resulting error (record), pipe the above to
    2>&1 | Format-List -Force; in PowerShell (Core) 7+, you can alternatively use the dedicated Get-Error cmdlet.

    Alternatively, you can bypass the explicit construction of [ErrorRecord] altogether and use a combination of different parameters to achieve the same result:

    Write-Error -Exception InvalidOperationException `
                -ErrorId OperationTest `
                -Activity MyActivity `
                -TargetObject MyErrorTarget
    

    I've found a bug in PowerShell that has pervaded since PowerShell 1.0.

    It looks like it.

    The likeliest explanation is that direct construction of [ErrorRecord] in PowerShell code requires advanced knowledge and is rare - in fact, letting Write-Error construct such an instance for you is its primary purpose, offering the familiar command-line experience of a cmdlet with parameters, as shown in the last workaround above.

    Another factor is probably that few callers that handle error records pay attention to fields such as .CategoryInfo.Activity; typically, it is the .ErrorId field or the wrapped exception's .NET type that is considered.
    On a more philosophical note, one could argue that the ErrorRecord type is a bit over-engineered.


    As for your more general observation:

    there's no real instruction on when to use the different types [of errors]

    • GitHub documentations issue Our Error Handing, Ourselves - time to fully understand and properly document PowerShell's error handling aims to provide an overview of PowerShell's surprisingly complex and partially inconsistent error handling and asks that such an overview become an official part of the documentation.

    • This answer attempts to provide guidance as to when to use non-terminating vs. statement-terminating errors.

      • That there are two types of terminating errors - script-terminating (fatal) vs. statement-terminating, and that throw in PowerShell code only ever creates the former, whereas .ThrowTerminatingError() (typically only used in C# code) only ever creates the latter is a confusing asymmetry - this comment on GitHub issue #14819 makes the case for abandoning this distinction in favor of offering only script-terminating errors in a future PowerShell version - i.e. the only kind of terminating error would then be one that is fatal by default. Doing so would:
        • prevent further commands in a script from executing by default, which seems called for, given that what are currently statement-terminating errors are usually severe error conditions.
        • eliminate the confusing asymmetry between the common -ErrorAction parameter having no effect on statement-terminating errors, whereas preference variable $ErrorActionPreference, when set to 'Stop', does (promotes them to script-terminating errors).