Concise error handling in Go with Rust-like Result types
Tags: go, rust, programming, softwareengineering
Published: 2023-06-10 17:32:44

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`](https://doc.rust-lang.org/std/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! 

```go
if err != nil {
    return nil, err
}
```

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.

```go
type Result[T any] struct {
	Ok  T
	Err error
}
```

## 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:

```go
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}
	}
}
```

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:

```go
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
```

Into this:

```go
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}
```

## 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](https://cs.opensource.google/go/go/+/master:src/encoding/json/encode.go) 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](https://github.com/olevski/eh). Give it a try and let me know what you think.