When managing errors in your code, help out your future self (and anyone else who will be supporting your code in the future) by doing more than the simplest possible thing.

A common approach in Go code is to detect an error and simply pass it up the chain:

err := manager.Process()
if err != nil {
    return nil, err
}

We relearned while working on Azure Service Operator (ASO) that this approach has all the same flaws as naïve exception handling in C# – by the time details of the error appear in a log file (or are otherwise made available for consumption), it has no context and is difficult to troubleshoot.

In C#, the idiomatic way to manage this is to catch the original exception and wrap it in another for context:

try
{
    manager.Process();
}
catch (Exception ex) 
{
    throw new InvalidOperationException(
        "Unable to process events", 
        ex)
}

In ASO we made it a standard to (almost) always wrap errors with additional context before passing them up the chain, using the package github.com/pkg/errors:

err := manager.Process()
if err != nil {
    return nil, errors.Wrapf(err, "unable to process events")
}

A bonus feature of errors.Wrapf() is that it returns nil if the error passed in is already nil.

So instead of writing this at the end of a function:

err := // … elided …
if err != nil {
    return errors.Wrapf(err, "unable to process events")
}
 
return nil

You can just write:

err := // … elided …
return errors.Wrapf(err, "unable to process events")

On first encounter, this violated the principle of least surprise, but in the Go ecosystem, this is idiomatic behaviour.

The only exceptions where we didn’t wrap errors was where we could easily verify that the error already contained all the context we’d need.

We found that this made our errors much longer - and much, much, more informative, making troubleshooting much easier.

Comments

blog comments powered by Disqus