I wrote about creating an Out-Error function before to avoid outputting the stack dump that a normal Write-Error displays.  Many newcomers to PSH take one look at the huge blob of angry red text and throw up their hands in frustration.

Write-Error is useful in populating $Error, which is very useful for exploring why a script went wrong.  One of the nice bits of data for debugging is $Error[0].InvocationInfo.ScriptLineNumber, which says where the script died.

Unless you use an Out-Error function, that is.  Then ScriptLineNumber points into the middle of Out-Error, not the function calling Out-Error.

One way to get around it is to use a ScriptBlock inline.  ScriptLineNumber will still point to the line calling the ScriptBlock instead of the actual offending line, but that is at least closer.

Here’s how to call it (shown first because it’s shorter):

Get-Item -Path NonExistentFile.txt -ErrorAction SilentlyContinue -ErrorVariable errorVariable
if ($errorVariable) { & $complainAbout $errorVariable.Exception.Message; }

This is what it returns:

WARNING: function Lib_O365IEAutomation.psm1 {} Cannot find path 'C:\NonExistentFile.txt' because it does not exist.

Here are the guts:

if (!(Test-Path -Path Variable:complainAbout))
{
    $complainAbout = 
    {
        <#
        .synopsis
        An over-engineered way to write a Warning to screen.
    
        .description
        Creates a silent $Error (with the correct Invocation.ScriptLineNumber)
        and outputs a Warning, both with the same message constructed from
        $args.

        This shows a warning message to the user without the Write-Error stack
        dump, which PSH novices may find intimidating / user-unfriendly.
    
        .notes
        Calling this means $error[0].InvocationInfo now contains the correct 
        ScriptLineNumber for where it died.  Creating an Out-Error {} function 
        does not - ScriptLineNumber would be in the middle of the Out-Error 
        function, not the script line that called Out-Error.
    
        .example
        return (& $complainAbout "message about what went wrong."

        Complain and stop executing.  

        .example
        & $complainAbout "message about what went wrong."

        Complain, but continue executing.
        #>

        ### how were we called
        $_myInvocation = Get-Variable -Scope 1 -Name MyInvocation -ValueOnly;

        if ($_myInvocation.CommandOrigin -eq 'Internal')
        { # if we're called from a function

            $message = "function $($_myInvocation.MyCommand.Name) {} "

        } # if ($_myInvocation.CommandOrigin -eq 'Internal')
        else
        { # we're called from a script

            $message = $_myInvocation.MyCommand.Name + " ";

        } #  if ($_myInvocation.CommandOrigin -eq 'Internal') ... else

        ### did we get called with a message 
        if ($args.Count)
        {
        $message += [string]::Join(" ", [string[]]$args).Replace("[\r\n]", " ");
    
        } # if ($args.Count)
        else
        {
            $message += "hit an undefined error.";
    
        } # if ($args.Count) ... else
    
        ### do the heavy lifting
        Write-Error -ErrorAction SilentlyContinue -Message $message;
        Write-Warning -Message $message;

    } # $complainAbout =

} # if (!(Test-Path -Path Variable:complainAbout))