All-in-all, I am a big proponent of the errors-as-values approach to error handling in Go. I definitely agree with the general wisdom of handling errors when and where they occur, and not delegating them using exceptions. So this post is not about how many people find needed to handle errors through code an annoyance. To me, that is just part of what it takes to write robust programs.

Instead, what I have found is that instead of just errors, I want to keep track of status, which can be an error, or it can be information about a success. Where this is useful is when writing packages designed to be used consumed by more than one context, specifically in my cases either a CLI or an API.

In the case of the API I want to be able to return meaningful message like “widget ‘foo’ created.” The idiomatic 2nd parameter that typically returns an error is a perfect affordance for storing this information.

However, by storing success information in what has been used only for errors we violate the Go error idiom and break any code that calls our code but assumes the err != nil approach to testing for errors.

What could an alternate idiom be, assuming the Go language designers had chosen that route, or might choose that route for Go 2.0?

Go could offer a built-in iserror(err error) function which could test for either err == nil meaning “no error”, or if err != nil then is could check to see if it implements IsError() and if so test for error using the IsError() method instead.

Here is what the code for iserror() could look like:

type ErrorIser interface {
IsError() bool
}
func iserror(err error) bool {
if err == nil {
return false
}
ei, ok := err.(ErrorIser)
if !ok {
return true
}
return ei.IsError()
}

With the iserror() method the idiom could change from this:

if err != nil {
return err
}

To this:

if iserror(err) {
return err
}

Another plus would be less typing of exclamation points (!) minimizing medical bills related to carpal tunnel syndrome! But I digress…

Anyway, if iserror() were idiomatic I could implement a status package and pass around an object that implements a Status interface instead of an error, and know that my code would not break idiom.

Here is a hypothetical status package (which is simpler than what I am actually using):

package stat
import "fmt"
type Status interface {
Message() string
Cause() error
IsError() bool
}
type status struct {
success bool
message string
cause error
}
func Wrap(err error,msg string) Status {
return &status{
success: false,
message: msg,
cause: err,
}
}
func Success(msg string, args ...string) Status {
return &status{
success: true,
message: fmt.Sprintf(msg, args...),
}
}
func (me *status) Message() string {
return me.message
}
func (me *status) Cause() error {
return me.cause
}
func (me *status) IsError() bool {
return me.cause != nil || !me.success
}

Using the above status and a desired iserror()function enabling a revised idiom we could write code like this:

func (me ProjectMap) AddProject(name string) (status Status) {
for range "1" {
status = ValidateProjectName(name, &ValidateArgs{
MustNotBeEmpty: true,
MustNotExist: true,
})
if iserror(status) {
break
}
p, status := NewProject(name)
if iserror(status) {
break
}
me[name] = p
status = Success("project '%s' added",name)
}
return status
}

Of course, I can implement the iserror() method today and start using it immediately — which in actuality I am already doing — but I cannot change the idiom across the Go community that all other Go programmers are familiar with and encouraged to use and so I can’t publish my code that uses this approach with about shame and lots of caveats.

FWIW.

Leave a comment

Your email address will not be published. Required fields are marked *