My take on Go2 and Error handling

Lately, there has been a lot of talk about the proposal of introducing a new way of handling the if err != nil { return err } pattern that seems to be loved by some and hated by others. It will often stir up a flame war when brought up among gophers. There are countless issues on this topic on golangs github and blogs written about it and I guess this yet another.

The initial proposal was brought forward in 2018 when the content of Go2 was pitched to the community and it can be found at https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md
As well as the following discussion and feedback at https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback

The try statement

The proposal that recently was backed by the core team was the try statement which would look something like the following

val, err := someFunc()
if err != nil {
    return err
}

//could be written as
val := try(someFunc())

At first glance, this looks like much of what a lot of people have been asking for in go and would more or less be implemented by rewriting the code in compile time to the original way of handling errors.

This proposal was recently rejected due to the feedback from the community and I think this proposal process was handled very well by the core team. Building proof of concept and letting people try it out and find problems and benefits with it.

Problems with try

I think it was good that this was rejected. There are a lot of potential issues with the try statement and I encourage you to read about it. However, the main problem I saw with it was not the functionality of it but rather the abstract concept of it.

Go is a minimalist language in terms of functionality, and that’s why many love it. It has had a big focus on orthogonal constructs meaning that there should only be one way of doing things. The error interface is in the language specification as a pre-defined interface, but I see no real reason for it being there conceptually (I do however respect it being there, I’m sure putting it in the language spec instead of the std lib has been given considerable thought). In my mind it’s not a language construct but rather a way people choose to write code, returning a value implementing an interface. While representing errors by the error interface are commonplace, there is nothing that forces you into it and there are projects that have gone with other ways of doing this.

So why do I think try was a bad idea? It would have added something to the language to handle a specific interface which, in my mind, conceptually does not belong in the language specification. Meaning that you would create somewhat of a chicken and egg problem. Go does not force you to handle errors through the error interface, which is a good thing, and I think it would have been a mistake to further integrate the interface into the language spec. Eg. Another common pattern is to return ok instead of an error, val, ok := myMap["key"] and any solution should, in my mind, consider not only things implementing the error interface but also any other value-based error handling.

A way forward?

Don’t get me wrong here, nowadays I don’t mind if err != nil { return err } checks all over the place. But I did at one point and in many cases, it does obfuscate what the code does. So what is it that I would like to see in a potential solution. I would argue that we want to see a language construct that “happens” to solve the issue but is not bound to that use case

  • An orthogonal concept to existing constructs
  • Generic value agnostic concept, not bound to a particular value type or interface
  • It should feel like go
  • Keep the “Sad path first” concept
  • Allowing for handling cleanup in case of error

So what would a potential solution be?
I’m not sure. But I think it would be interesting to explore a left-hand function call with a “super return”

eg.

func something() error{

    errcheck := handle(err error){
        if err != nil{
            return err
        }
    }

    
    errcheck(err) := SomethingErrorProne()
    key, errcheck(err) := SomethingElseErrorProne()

    
    ok2Err := handle(ok bool, message string){
        if !ok {
            return errors.New(message)
        }
    }
    
    val3, ok2Err(ok, "could not find key in map") := amap[key]
    
    
    sanity := handle(min, max int){
        if min > max {
            return errors.New("did not return correct min max")
        }
    }
    
    sanity(min, max) := findMinMax([]int{5,1,2,4,7,2})
    fmt.Println(min, max)
   
    return nil
}


Author:
Rasmus Holm, CTO/Developer