Hello all,
I’ve been writing a lot of functions recently, and while I haven’t been all that consistent about it, the most common way I’ve had the functions deal with non-fatal errors is to write-error the problem and then return a sentinel value (e.g. an empty string or $false or $null) as their output.
As an example, let’s say I have a function that’s performing a REST API call that in one case returns an error (e.g. because the object it’s looking for in the REST app doesn’t exist). There are scenarios where this error is fatal and the code needs to immediately exit non-zero, but there are also scenarios where that error would be unexpected but not fatal (e.g. the object referenced is a bit player in some noncritical metadata), in which case I would write-error that the object wasn’t found and then return $false. I’d prefer to handle more of that logic in the calling code, so the caller decides whether to write-error, write-warning or just silently continue, but in that case I need the error data to be returned.
As my PowerShell scripting has scaled up drawbacks like the one I just mentioned are beginning to mount and I’m hoping to adopt a more mature approach.
What I’m leaning towards is creating a standard struct that my functions will return that looks like this:
add-type -TypeDefinition @"
public struct ReturnValue {
public bool IsSuccess;
public object GoodResult;
public object ErrResult;
}
"@
function HelloWorldReturnValue() {
[CmdletBinding()]
param()
[ReturnValue]$output=[ReturnValue]::New();
$output.IsSuccess = $true
if (1 -eq 1) {
$output.GoodResult = "Hello, World"
}
else {
$output.ErrResult = "oh no"
$output.GoodResult = $false;
}
write-host "done"
return $output;
}
[ReturnValue]$HelloWorldResult = HelloWorldReturnValue;
write-host "Success: $($HelloWorldResult.IsSuccess)"
write-host "Good: $($HelloWorldResult.GoodResult)"
Before I adopt the above approach in all my code I’m interested in learning whether there are better alternatives. I thought about about using Information Streams, e.g.:
function HelloWorldInfoStream() {
[CmdletBinding()]
param()
Write-Information 'About to say hello' -Tag LogVerbose
write-information "Hello, World" -Tag MainOutput
write-information $true -Tag IsSuccess
write-host "done"
}
These are great in that they provide infinite flexibility in tag naming, but I don’t think I can specify that a given tag always be typed a certain way. Plus it seems to require more ceremony to parse, e.g.
$x = HelloWorldInfoStream -InformationVariable retInfo
if (! ($retInfo |where-object {$_.tags -eq 'IsSuccess'}) {
write-error $retInfo | where-object {$_.tags -eq 'LogError'}
exit 1;
}
Another option would be reference variables:
Function HelloWorldRef( [Ref]$IsSuccess) {
write-host "isSuccess is $($isSuccess.value)"
$isSuccess.value = $true;
return "done"
}
$x = $false;
$response = HelloWorldRef -IsSuccess ([Ref]$x)
These are also very flexible but I find that reference variables aren’t all that well understood by non-programmer IT scripters. Plus (and perhaps this is my own ignorance) I haven’t found a way to declare a [ref] parameter as a specific type and still be able to name that parameter in the calling code. That said, this is probably my second choice right now.
Anyway, these are some of the ideas I’ve been playing with. I’m sure greater minds than mine have considered these and others, and would welcome any feedback on a better solution.
Thanks!