DEV Community

Cover image for Concise error handling in Go with Rust-like Result types
Tasko Olevski
Tasko Olevski

Posted on

Concise error handling in Go with Rust-like Result types

I have recently started learning Rust and I really like the utility and brevity of the Result enum and the ? operator. So I tried to see how close I can get to implementing something like that in Go. In addition, I can point to this when someone says something along the lines of "Oh Go error handling is so verbose".

If you are unfamiliar with Rust, the Result type is an enum that can hold either an Ok valid value or an error. This is pretty neat but the real utility comes from the ? operator. When the ? operator is called on a Result it will stop the execution of the code if there is an error in the Result and simply return the Result from the parent function. Basically Rust replaces the well known Go error handling boilerplate like below with only a single character!

if err != nil {
    return nil, err
}
Enter fullscreen mode Exit fullscreen mode

If there is no error in the Result, then the operator unwraps the Ok value and returns it.

Rust and Go are quite different, so implementing the same Result enum and ? operator from Rust in Go is not as simple as porting or translating the code. Here are some of the challenges I came across and how I addressed them in Go.

Enums

I don't think it is a hot take to say that Go's enums can be limiting and are not nearly as expressive as Rust's. Therefore instead of using an enum in Go for the Result type I used generics and a struct.

type Result[T any] struct {
    Ok  T
    Err error
}
Enter fullscreen mode Exit fullscreen mode

Question mark operator

This is a bigger problem. As far as I know Go does not have any functionality similar to this. The only way to break the flow of a function "at will" is to panic. So I decided that I can add a method on the Result struct that will panic if an error is present. But that is not all, I still need some way to recover this panic and let the error "bubble up" the call stack. The way to achieve this is to return a named Result from the parent function, pass a pointer to this named Result to a deferred function that will recover from the panic and place the error in this pointer to the output. Let's call this deferred function EscapeHatch. This is how it would look like:

func EscapeHatch[T any](res *Result[T]) {
    if r := recover(); r != nil {
        err, ok := r.(ehError)
        if !ok {
            // Panicking again because the recovered panic is not an ehError
            panic(r)
        }
        *res = Result[T]{Err: err.error}
    }
}
Enter fullscreen mode Exit fullscreen mode

One more thing comes up here. Namely, any panics that we raise through the ? operator are wrapped in a simple struct so that we recover only those and for all others we panic again. Lastly, because ? is not a valid name for a method in Go I instead used Eh. This is short for EscapeHatch and also something that Canadians will add at the end of a sentence to make it into a question. For example "We have been having some lovely weather, eh?". So I think it is an appropriate substitution to the ? operator.

Demo time!

Putting everything together I can convert code like this:

func example(aFile string) ([]byte, error) {
    f, err := os.Open(aFile)
    if err != nil {
        return nil, err
    }
    buff := make([]byte, 5)
    _, err := f.Read(b1)
    if err != nil {
        return nil, err
    }
    return buff, nil
Enter fullscreen mode Exit fullscreen mode

Into this:

func example(aFile string) (res eh.Result[[]byte]) {
    defer eh.EscapeHatch(&res)  // must have pointer to the named output Result
    buff := make([]byte, 5)
    file := eh.NewResult(os.Open(aFile)).Eh()
    _ = eh.NewResult(file.Read(buff)).Eh()
    return eh.Result[[]byte]{Ok: buff}
Enter fullscreen mode Exit fullscreen mode

Isn't this really hacky?

I felt a lot worse about this idea initially, especially about potentially "abusing" panic in this way. But then I found out that even the json package in the standard Go library will use panic, wrap the error and recover it to stop executing when recursively encoding an interface. So if this is an anti-pattern then at least I can say the standard library is doing it too.

Shameless library plug

If you are curious I combined all the code and some utility functions in a small package here. Give it a try and let me know what you think.

Top comments (2)

Collapse
 
zrbecker profile image
Zachary Becker • Edited

Found your article while reading some Rust material and wondered if anyone had attempted this in Go.

I think it's a bit awkward using it with the standard library calls, but in theory if this Result[T] pattern was more popular there would probably be someone who wrapped the standard libraries to return a Result[T] instead.

I came up with something like looked like this

func ReadFile(name string) (res wrap.Result[[]byte]) {
    // We require a deferred call to UpwrapHandler to safely use Unwrap
    defer wrap.UpwrapHandler(&res)

    file := wos.Open(name).Unwrap()

    buf := make([]byte, bufferLength)
    length := wos.Read(file, buf).Unwrap()

    return wrap.OK(buf[:length])
}
Enter fullscreen mode Exit fullscreen mode

Here is the rest of the source for reference: github.com/zrbecker/wrap/

Very neat pattern for sure. I also found handling the panic to not feel as dirty as I thought it would.

Collapse
 
olevski profile image
Tasko Olevski • Edited

Hi Zachary, I thought I would get notifications if someone comments on my article but I guess I did not or it just got lost in my inbox among the mostly unimportant stuff I get on a daily basis.

And yeah I agree since no other package or library is using this it gets awkward because you have to wrap every output from a function that comes from another library.

Maybe a future version of Go will support fancier enums like this and the standard Go library will have Result/Option types just like Rust 🤞.