Part 2 is here

I debug code and think that software engineering principles would be nice. I don’t understand them but I think they are nice.

Really, there are a lot of concepts from software engineering that should be applied once your powershell applications, for that is what they are, start getting larger.

I’m currently working on a project that has multiple layers of wrapping. Our API has a client module already built. I’m writing a module to transform that output so it’s easier to consume in the logic part of that code, and also so I can slot in another API later. This leads to the problem of how to handle error conditions.

Say you have:

process {
    foreach ($Item in $InputStream) {

        # ...27 lines of logic about processing the input...

        Write-Output $Item.RelevantProperty
    }
}

Well, if I already have a lot of branching, the last thing I want to do is clutter it even further with exception and null handling.

A concept that’s been around since donkey’s years in .NET is delegates, which is a way to make a type out of the concept of a callback function. (It doesn’t have to be a callback, but that’s how I’m going to use it here.)

Side note: I am not a C# developer. I don’t want to give the idea that this article covers much of the topic of delegates. I’m always open to having any misconceptions cleared up.

In Powershell terms, think of a delegate as passing a scriptblock that has a strongly-typed param block and return type. So, conceptually:

#In the function that gets called:
param(
    [string]$MainParam,
    [string]$OtherParams,
    [System.Delegate]$ErrorCallback
)

#Could also use try/catch
trap [System.Web.HttpException] {$ErrorCallback.Invoke($_.Exception)}

#  ...main processing...


#In the calling function:
$Scriptblock = {
    param([Exception]$Exception)
    if ($Exception.Message -match '401') {Get-Credential}
}

Invoke-LowerLayer -MainParam $blah -OtherParams $blahblah -ErrorCallback $Scriptblock

In this code, any exceptions that happen get passed upwards through a side channel. Your main output stream contains only the objects you care about, and you don’t have to clog up your logic with error-handling. Your error-handling code is separate to your main logic. And if that doesn’t make you smile, you haven’t inspected much code ;-)

Now, you don’t actually pass a System.Delegate. You’ll pass a System.Action. We can use that as a generic type, so we’ll specify some other type or types at the same time, which map to the types of the parameters that your scriptblock accepts. Because we’ve got param([Exception]) above, we’ll have param(Action[Exception]) below. Thus:

#On the function you call:
param(
    [string]$MainParam,
    [string]$OtherParams,
    [Action[Exception]]$ErrorCallback
)

#In the calling function:
$Scriptblock = {
    param(
        [Exception]$Exception
    )
    switch -Regex ($Exception.Message) {
        '401|Token' {
            Update-StoredCredential
        }
        '403' {
            Write-PopupMessage "ACCESS DENIED"
        }
        default {
            Write-PopupMessage $_
        }
    }
}

Invoke-LowerLayer -MainParam $blah -OtherParams $blahblah -ErrorCallback $Scriptblock

Which we can refine further:

#On the function you call:
param(
    [string]$MainParam,
    [string]$OtherParams,
    [Action[Exception, System.Management.Automation.CommandInfo, hashtable, bool]]$ErrorCallback
)

#In our try/catch or trap:
 $IsRetryable = $(some logic)
 $ErrorCallback.Invoke($_, $MyInvocation.MyCommand, [hashtable]$PSBoundParameters, $IsRetryable)


#In the calling function:
$Scriptblock = {
    [CmdletBinding()]
    [OutputType([void])]
    param(
        [Exception]$Exception,
        [System.Management.Automation.CommandInfo]$Caller,
        [hashtable]$CallerParameters,
        [bool]$Retryable
    )
    switch -Regex ($Exception.Message) {
        '401|Token' {
            Update-StoredCredential
        }
        '403' {
            Write-PopupMessage "ACCESS DENIED"
        }
        default {
            if ($Retryable) {
                Start-Sleep 5;
                & $Caller @CallerParameters
            }
        }
    }
}

By passing the extra parameters, we can retry the original call.

Note that I’m casting $PSBoundParameters to hashtable because that object doesn’t contain Clone() and I don’t want to pass a reference to an object that may still be being used. (‘Be being’?)

Finally, can we send in a function definition? Is cheese tasty..?

function Handle-Error {
    [CmdletBinding()]
    [OutputType([void])]
    param(
        [Exception]$Exception,
        [System.Management.Automation.CommandInfo]$Caller,
        [hashtable]$CallerParameters,
        [bool]$Retryable
    )
    switch -Regex ($Exception.Message) {
        '401|Token' {
            Update-StoredCredential
        }
        '403' {
            Write-PopupMessage "ACCESS DENIED"
        }
        default {
            if ($Retryable) {
                Start-Sleep 5;
                & $Caller @CallerParameters
            }
        }
    }
}

Invoke-LowerLayer -MainParam $blah -OtherParams $blahblah -ErrorCallback (Get-Item Function:\Handle-Error).ScriptBlock

Note that this code doesn’t function as intended - the solution is in part 2

The benefits of this approach are:

  • The type system will help your colleagues to use it correctly
  • It’s easy to wrap layer upon layer upon layer
  • Error handling is kept separate from business-as-usual processing
  • Other benefits that I’ll think of after I click ‘Publish’

Now, about that code snippet not working. I wanted to show, conceptually, that you can pass CommandInfo through and invoke it (CommandInfo is the type you get from Get-Command). Two things:

  • You wouldn’t pass an Exception and a CommandInfo in Powershell, you’d just pass the ErrorRecord. ErrorRecord already contains the Exception and the invocation details of whence it was thrown. It’s a nice little wrapper class.
  • Whatever scriptblock you cast to Action will never return any output. Action returns void.

There’s more in Part 2, but I won’t blame you if that’s enough for now.

Happy callbacking!

Part 2