Search code examples
powershellclasserror-handlingwrite-error

Write-Error doesn't result useful information when used inside class methods | Powershell


I used a method with and without a class and the Write-Error seems to produce different outputs. In case of class, it doesn't specify the function and the line number is always 1,1

function oper1() {
    Try {
        [string] $cmd = ".\some_exe_which_does_not_exist.exe"
        iex $cmd 
    }
    Catch {
        Write-Error $_.Exception.Message
    }
}

oper1

Output for above:

oper1 : The term '.\some_exe_which_does_not_exist.exe' 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. At F:\debug\encryption_concat_tests\Untitled1.ps1:11 char:1 + oper1 + ~~~~~ + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,oper1

When I enclosed the same function in a class, I got this:

class Operator {
    [void] oper1() {
        Try {
            [string] $cmd = ".\some_exe_which_does_not_exist.exe"
            iex $cmd 
        }
        Catch {
            Write-Error $_.Exception.Message
        }
    }
}

[Operator] $operator = New-Object Operator
$operator.oper1()

The term '.\some_exe_which_does_not_exist.exe' 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. At line:1 char:1 + F:\debug\encryption_concat_tests\Untitled1.ps1 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException

What could be the reason for this behaviour for methods inside classes?


Solution

  • As an aside: Invoke-Expression (iex) should generally be avoided; definitely don't use it to invoke an external program - just invoke it directly, as shown below.


    In PowerShell class methods:

    • Do not use Write-Error, as classes are not designed to emit non-terminating errors.

      • The only reason you're seeing any output at all is a bug as of PowerShell Core 7.0.0-rc.3 with methods whose return type happens to be [void] - see GitHub issue #5331.
    • Instead, communicate errors solely by throwing them with the Throw statement or by not catching terminating errors (which include exceptions from .NET methods and cmdlet calls with -ErrorAction Stop).

      • Note: Throw and -ErrorAction Stop (or $ErrorActionPreference = 'Stop') create script-terminating (thread-terminating) errors, whereas exceptions thrown by a .NET method (not caught and re-thrown in the class method) only create statement-terminating errors; that is, while the class-method body is terminated right away, execution continues in the caller by default; the latter also applies to the call operator (&) not finding an executable, errors in expressions such as 1 / 0, and cmdlet calls that emit statement-terminating errors (the most severe error type they can report) without their being promoted to script-terminating ones with -ErrorAction Stop; see this GitHub docs issue for a comprehensive overview of PowerShell's complex error handling.

    See this answer for more information about error handling and stream-output behavior in class methods in particular.

    Here's a corrected version of your code.

    class Operator {
        [void] oper1() {
            Try {
                # Try to invoke a non-existent executable.
                & ".\some_exe_which_does_not_exist.exe"
            }
            Catch {
                # Re-throw the error.
                # Alternatively, don't use try / catch, but the error
                # then only aborts the method call, not the entire script.
                Throw
            }
        }
    }
    
    $operator = [Operator]::new()
    $operator.oper1()