Best practice for returning errors

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!

What’s not clear is whether, when an error occurs, you expect the called function to continue running or whether your called function does one thing and then returns to the calling function.

If it’s the former, then I think the approach of returning a custom class, or even just a hashtable with all the information for the caller to process is valid.

However, if your function only does one thing, then Write-Error with -ErrorAction Stop or throw{} would be better; you can then use try/catch in your calling function to handle the error.

1 Like

The overall approach I take with large scripts with lots of function calls is to use a common logging mechanism/function that works in conjunction with Start-Transcript to catalog Info, Warnings and Errors. As Matt stated, the intent of what happens on errors is important. In my case, I want the script to finish, then process the log and deal with it.

FWIW