Golang 'error': Everything you know is wrong
Tags: go, programming, design, errors
Published: 2022-08-04 04:45:38

## "errors as linked lists"

### The Attempted Concept

The concept of errors as linked lists seems neat at first. You can define your own `error` type, customize how it prints, and add extra information to it. Once instantiated, you can simply pass it up to the caller as a normal old `error`. Callers of your function can then check to see if your `error` `Is()` another. Cool! Further even, you can add extra info to an `error` that you wrap and bubble up. Awesome! `error` is so flexible and powerful!

### `error` Chain Of Pain

You start setting up error handling in your first golang application and you find that you start running into problems. You find that you cannot both return an `error` that `Is()` both your custom error and the `error` you just received. "That's fine", you think to yourself, "I should not have the caller rely on my implementation details anyway". So you return your API's own `error` and move on.

You then start working with some HTTP helper library that returns an `error` if a request fails. Suddenly, you realize that you also need to know if that `error` `Is()` a `context.DeadlineExceeded` from the request timing out. "That's fine", you think again, "I know how to handle that!". That is when you realize that the `DeadlineExceeded` error most likely originates from deep within that call stack and you would be relying on _their_ implementation details. Even worse, you realize, "What if the library I am using does not bubble up that `error`, just as I did earlier? After all, that library could be catching it and returning their own errors too, or worse, that library calls yet another one that does". Suddenly, you realize that everything you knew and trusted about golang errors is wrong. You can no longer trust the `error` chain to provide reliable consistent results from `Is()`.

### `error` Format Catastrophe

I would suggest reading this blog about [the catastrophe of golang error printing](https://dr-knz.net/go-error-printing-catastrophe.html). This blog discusses yet another failure of the golang `error` pattern, that if you have a chain of errors and if _any_ of them do not implement `Unwrap() error` (an optional interface implementation for `error`s), then your whole chain breaks down. It also goes into detail about the issue of calling `Format()` on the `error` chain.

### `error` Handling Problems

One issue that many golang devs might not realize is that errors are constantly being shadowed. This is not usually a problem because once an `error` is encountered, generally, the function returns. Consider the following:
```
if fooResult, err := foo(); err != nil {
    if barResult, err := bar(); err != nil { // err shadows previous err
        ...
    }
}
```
The `err` returned by `bar()` shadows the `err` returned by `foo()`. This is probably not going to cause any problems but you do end up with two instances of `err` in the same function. Anytime you have shadowed variables it is a code smell. Unfortunately, that means all of golang error handling smells. 

[Side note](https://go.dev/play/p/wwGjCj9rNu4): golang gets away with this because it makes an exception on the `:=` operator that if at least one variable is newly declared, then it is fine. That is why you can redeclare `err` constantly. It just bends the rules a bit to fit the pattern, but causes variable shadowing in the process.

The only way to solve this is by not shadowing those variables, but the solution is not pretty to read:
```
var fooResult struct{}
var err error
if fooResult, err = foo(); err != nil {
    var barResult struct{}
    if barResult, err = bar(); err != nil {
        ...
    }
}
```
So the solution is to declare your variables before assignment. Following that train of thought, if you think this issue is solved with named returns, think again. Let's take [this example](https://go.dev/play/p/8vjo3ZDcnlu):
```
func task() (err error) {
    if err = foo(); err != nil {
        if err := bar(); err != nil {
            ...
        }
    }
    return
}
```
Whoops! You just typo-ed that `:=` on `bar()`. The compiler says,👍. The function works. But only `foo()` errors will ever be returned. This is a logic bug, plain and simple. It is easy to fix, but could be difficult to find.


### The "Solution"

How do you solve these problems? Short answer: you don't. At least, not without changing the core golang pattern and probably having a few debates with your coworkers about best practices. This is a problem the language itself must address.

## Experimental Alternative

Frustrations with `error` has led me to the propose following changes (that you can try right now!):
- Abandon `error` and never look back
  - `errors` package becomes dead code as a result
  - `fmt` directive `"%w"` can go away too
- Make a `type Error struct {}`
- Make errors values, not references
- `type error Error`, for good measure
  - Yes, you can do that. Get that thing outta here!
- Make a `type Result[T any] struct {}`

Now you can live in golang free of the burdens of the built in `error` interface. For example:
```
func task() Result[int] {
	fooResult := foo()
	if !fooResult.IsOk() {
		return fooResult
	}

	barResult := bar()
	if !barResult.IsOk() {
		return Err[int](barResult.Error())
	}

	return Ok(1)
}

func foo() Result[int] {
	return Ok(2)
}

func bar() Result[float32] {
	return Err[float32]("whoops")
}
```
[Goplay](https://go.dev/play/p/GDtuf6qJEhq)

This is a horrible example, but it demonstrates that you can easily change error handling for the better. Technically, this will impact consumers of this package who use golang `error`, but the issues this will cause are same that exist already. Broken `Unwrap() error` chains. Poor formatting standards. Leaking implementation details. You get the idea.

If you noticed, this proposed pattern solves the issues noted in the above section. Errors are values (no wrapping involved), so no more weird chaining issues. Error printing is reliable since there is a defined error struct. Each function or package can define explicit Errors as part of its API that can be checked for by equivalence, not by `Is()` chain detection. Result is a single value, so no more shadowed variables. If you want to add more info to your errors, you could embed `Error` in your own `type ApiError struct {}`.

A surprise benefit of this new pattern is that your errors no longer _automatically escape to the heap_ (hint: `error` is an interface). That's right. Not only is this pattern easier to use and solves the issues of standard golang `error`s, but it is more performant.

More work obviously needs to be put into this before it can be useful in an actual project, but this is definitely a great start.