Looking for opinions on error handling on internal function within public one

Hello,

I have been struggling to determine how to write internal functions, specifically the error handling component, as part of a public one I am writing. I have made no progress reading online blogs about it. Hopefully, someone can help me here.

Here is the pseudo code

function internal {
[cmdletbinding()]
param ()
# check for something here
if ($Check -eq $false) {
$ERec = [System.Management.Automation.ErrorRecord]::new(
[System.Exception]::new("check failed"),
"errorID",
[System.Management.Automation.ErrorCategory]::NotSpecified,
$null
)
$PSCmdlet.ThrowTerminatingError($ERec)
}
}

function external {
[cmdletbinding()]
param ()
try {
# some stuff done here
helper
# more stuff done here
}
catch { $PSCmdlet.ThrowTerminatingError($PSItem) }
}

When I run the external function, and the helper function internal check fails and throws a terminating error, which then triggers the try-catch block of the external function, I see what I would expect on the screen which is…

external : check failed
At line:1 char:1
etc etc

However, when I look at $Error, I see…

internal : check failed
At line: 24 char: 9
etc etc

When I compare this to a cmdlet like Connect-MSOLService, and purposely fail the authentication, the $Error variable does not expose the internal function function name, line, and other details. It also shows in red text, and my function error record does not.

Where am I going wrong?

Extra context: I am essentially writing a connect type function for an application, so I am trying to mimic cmdlets like Connect-MSOLService, and the internal function example is to check for authentication errors.

When you catch an exception, you can choose what to do with it. If you simply re-throw it, then you are getting the stacktrace or bubbling up the lower error. You can also do a if or case statement and then create another exception message. if you haven’t seen this blog, it covers just about everything you want know about exceptions:

Here is a basic example:

function Get-Internal {
    [cmdletbinding()]
    param ()
 
    try {
        Get-ChildItem 'C:\PathDoesNotExist' -ErrorAction Stop
    }
    catch {
        Throw $_
    }
}

function Connect-External {
    [cmdletbinding()]
    param (
        [string]$ComputerName
    )
    try {
        Get-Internal -ErrorAction Stop
    }
    catch { 
        #Bubble up the exception of the Get-Internal Call
        #Throw $_

        #Create a basic message
        $msg = 'Connection failed to host {0}' -f $ComputerName
        #Throw $msg

        #Throw a specific exception
        #Throw [System.Security.Authentication.AuthenticationException]::new($msg)
        throw [System.Security.Authentication.AuthenticationException]::New($msg)
    }
}

try {
    Connect-External -Computername google.com -ErrorAction Stop
}
catch {
    Throw $_
}

Output:

Connection failed to host google.com
At line:31 char:9
+         throw [System.Security.Authentication.AuthenticationException ...
+         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (:) [], AuthenticationException
    + FullyQualifiedErrorId : Connection failed to host google.com
 

1 Like

Thanks, Rob. In your example of the error record, it shows at line 31 char 9, exposing the line within the function itself, rather than call of ‘Connect-External’. Why does it not do this for say the cmdlet ‘Connect-MSOLService’?

PS C:\Users\robert> Connect-MsolService
Connect-MsolService : Authentication Error: Unexpected authentication failure.
At line:1 char:1

  • Connect-MsolService
  • CategoryInfo : OperationStopped: (:slight_smile: [Connect-MsolService], Exception
  • FullyQualifiedErrorId : System.Exception,Microsoft.Online.Administration.Automation.ConnectMsolService

PS C:\Users\robert> $Error
Connect-MsolService : Authentication Error: Unexpected authentication failure.
At line:1 char:1

  • Connect-MsolService
  • CategoryInfo : OperationStopped: (:slight_smile: [Connect-MsolService], Exception
  • FullyQualifiedErrorId : System.Exception,Microsoft.Online.Administration.Automation.ConnectMsolService

The purpose of the exception isn’t to necessarily specify the line the error occurred in a function\sub-function. If you are throwing a good exception, you should be providing the end-user with a clear error on why Connect-MsolService failed, not where it failed. If Connect-MsolUser tells you that on line 25 in Connect-MsolService something failed, you may not even be able to see code or if you can have any idea how to troubleshoot it. The developer should be trying to tell the user, you tried to call Connect-MsolService and it didn’t work because because of X. Surprisingly, Connect-MsolService does not have any mandatory parameters which is interesting because you would think that connecting to a web service would require credentials. It would something like this:

function Connect-External {
    [cmdletbinding()]
    param (
        [string]$ComputerName
    )
    try {
        Get-Internal -ErrorAction Stop
    }
    catch { 
        
        switch ($_.ExceptionMessage) {
            "*host not found*" {
                $msg = 'Connection failed to host {0}' -f $ComputerName
                throw [System.Security.Authentication.AuthenticationException]::New($msg)
            }
            "*bad credentils*" {
                $msg = 'Credentials are incorrect. Try X and Y'
                throw [System.Security.Authentication.AuthenticationException]::New($msg)
            }

            default {
                $msg = 'Unexpected error occured. Maybe give some internal error from helper call.'
                throw [System.Security.Authentication.AuthenticationException]::New($msg)

            }
        }

    }
}

The more debugging a developer does, the more clear messages you should get before you get to the default error message like you are seeing. Regardless, telling a user what exactly failed internally isn’t the goal, it’s to provide a clear error on why it did not work and maybe what they should try. You entered bad credentials, please try to re-enter your creds…access denied. check your permissions to X.

1 Like