error handling in Go: more expressive syntax

update:

After I wrote the article, I found https://github.com/golang/go/issues/21161 . I’ll compare my idea with the comments there. Still I think that the next sections give some interesting background about melt and measuring expresiveness

I believe error handling in Go has its strenghts, but ultimately it is making the whole language too verbose. The issues with it are purely syntax-based. One can quantify how the current syntax makes Go unexpressive and one can combine all of the current error semantics with a more expressive syntax.

Why

I’ve had many discussions with people about Go error handling, read multiple articles and responses by Go community members, and very often people express opinions in the spirit of “I don’t care about the boilerplate, because it helps me to implement robust error handling”. I still can’t see why the boilerplate is needed to have this kind of handling.

Go can be more expressive

One can write a book on analyzing languages. In this article I’ll demonstrate only a simple measurement.

I’ve worked in the past on a compiler that generates idiomatic code in different languages. It supported Python, Ruby, C#, JS and Go output. In order to express most idioms, I defined them in a “standard language-agnostic library”. Most of them can be expressed as a typed function in the core language (with effects, I didn’t do that, but that would be a good next step).

The standard library was mapped to idiom templates or standard functions in each language. I had this concept of “block-injecting” (called leaking nodes in the code) translations. Most methods could be mapped to an in-place equivalent, but some of them required me to inject some code in the surrounding code block. That means the reader of the code has to follow a lot more, he has to context switch between several lines, to look at more temporary variables. And overally it leads to more complex code.

Most of the block-injecting translations were for generating idioms in Go. And if you have a call that might return an error, you can be sure a call chain including it will always translate to a multiline mess.

def f =
  b.x().y(2)
end

func f() (int, error) {
    t1, err := b.x()
    if err != nil {
        return 0, err
    }
    t2, err := t1.y(2)
    if err != nil {
        return 0, err
    }
    return t1, nil
}

When simple idioms from Lang with the same behavior need a lot more code which is more complex itself in OtherLang: that means the expressive power of Lang > expressive power of OtherLang.

(Yes, they are equivalent: in the ruby code you might call f in a begin block, but the call site won’t be more complex than calling it in go)

One can use this kind of “block-injecting” complexity and the resulting code length as a metric.

A solution

What if you could

func f!() int:
  return b.x().y(2)
  escalate x, y

Eventually I started using Go in my previous job. I love language development, so naturally I decided to write a simple language on top of Go, called Melt. One of the central ideas was to preserve Go’s error handling, just in a better package.

Error syntax in Go has some nice properties:

The problem is, it forces you to do it immediately on the next 2-3 lines. This makes composability and chaining of calls almost impossible very often:

Functions that return another object are most often called like obj, err := f() Your code demonstrates that it returns an error, even if you ignore it, you have to write _ Functions that don’t return anything except an error are often just written f() and Go doesn’t even warn you you might miss it.

An alternative solution is:

This satisfies Go’s requirements:

You can separate your logic from your error handling.

The next step is dealing in a generic way with functions that might or might not return an error.

An alternative solution:

This could’ve been used for generic error handling:

func Map?<T, U>(handler? T -> U, sequence Sequence<T>) []U:
    result = make([]U, len(sequence))
    for i, item in sequence:
        result[i] = handler?(item)

    escalate handler
    return result

(That was a Melt example). Here you can deal with handlers that return (U, err): Map would also returns ([]U, err) for them.

Very often, people just escalate their errors: they return err. In an alternative solution you can have a shortcut for that.

The resulting code looks a lot like exception-based handling, but it’s a more powerful version of it: no overhead and you can still immediately see how errors are propagated

Melt

I started implementing those ideas in Melt, but eventually I stopped writing Go and I got busy with other stuff. I decided to open source the existing code: I have abandoned the project. Currently I am very happy with Nim most of the time, so I don’t have the need to work on Go-related tools. It’s in very early stage, and it has only some examples: still, other people might find it useful or fork it for their needs.

Here it is: melt

That’s not true

I’ll be happy to discuss this: it’s probable that other people had similar “solutions” and that there are huge problems with my idea. I’d love to see some kind of proof that current Go error handling can’t be combined with more expressive syntax. I respect the Go community a lot and while I prefer languages with very different philosophy I can see most of Go-s pro-s. It works find for its problem domains. The error handling model is nice and the concurrency primitives are stunning. I don’t argue about generics in this article: I’ve seen the amount of research the community is putting there, and I know it will be hard to add them now. I think AST metaprogramming(I don’t think current codegen is good enough) and better syntax for error handling are the most important quality of life changes required for Go.

You can always ping me at my github address or in a reddit discussion.