Avoid go1.20 Error Handling
Tags: go, design, errors
Published: 2023-02-06 06:39:27

I have already stated just how bad golang error handling is in  [Golang Error Handling](https://dev.to/wspowell/series/19707). This post focuses on the new additions to error handling in go1.20.

## My hot take

My opinion remains that error handling provided by golang is not great, but I believe that the new go1.20 additions bring a whole new dimension of issues to the language that are just _staggering_. To put it bluntly, I have never seen a feature released by a language that actively makes the language _worse_. This and other bizarre decisions make me deeply concerned for the future of golang. If I ever see `errors.Join()` or `Unwrap() []error` in a code review, it will not get my approval. 

## Pre-go1.20

Let me show an example of the pre-go1.20 state of things just to emphasize the absurdity of these new changes. `error` is a linked list of any implementation, unknown to the consumer, that just has to satisfy `Error() string`. Let's take a look at what this would look like if we did not have a nice `error` word saving face.
```
// Go Playground: https://go.dev/play/p/f3Xwyv30C4U
type LinkedList struct {
	value any
	next  *LinkedList
}

func New(value any) *LinkedList {
	return &LinkedList{
		value: value,
		next:  nil,
	}
}

func Wrap(value any, currentList *LinkedList) *LinkedList {
	return &LinkedList{
		value: value,
		next:  currentList,
	}
}

func DoWork() *LinkedList {
	if resultLinkedList := addJob(); resultLinkedList != nil {
		return Wrap("DoWork failed", resultLinkedList)
	}

	return nil
}

```
This is effectively what is happening when you create errors and wrap them in golang. Note: I omitted the linked list traversal. I would be very concerned if someone tried to implement this error pattern in any language. It is complex, error prone, and an anti-pattern. It also leaks implementation details. Any consumer of `DoWork()` must also know what `addJob()` does and what its error returns mean. To say otherwise is to ignore the very nature of the linked list. This is not great in itself, but it gets even worse with go1.20.

## Enter go1.20

This update introduces two new additions to the `errors` package:
- errors.Join()
- Unwrap() []error

This means that we can now specify multiple errors when wrapping. For example: `fmt.Errorf("errors: %w, %w", err1, err2)`. If you notice, this also means that we now have to track multiple errors instead of just one per error instance, which means we have moved from a linked list to a tree of errors. Size and time complexity just increased. Think about the `LinkedList` example above and how that would become even more complicated.

So what does this even look like in go1.20. Here is a small example.
```
// Go Playground: https://go.dev/play/p/-iUZt7kSp6-
func externalFn1() error {
	return fmt.Errorf("%w OR %w?", err1, err2)
}

func externalFn2() error {
	return fmt.Errorf("%w OR %w?", err2, err1)
}

func printErr(err error) {
	switch {
	case errors.Is(err, err1):
		fmt.Println("err is err1 - " + err.Error())
	case errors.Is(err, err2):
		fmt.Println("err is err2 - " + err.Error())
	}
}

// printErr(externalFn1())
// prints -> err is err1 - one OR two?
// printErr(externalFn2())
// prints -> err is err1 - two OR one?
```
This highlights a massive issue with this: the error is both err1 and err2. However, when you check for those, you will never fall into the err2 case because err1 matches first. 

Because a function can now return _multiple_ errors, there is no way to determine what the _actual_ error is. How should a developer handle these errors? Maybe one function only has one error but there is the possibility that it could be more than one, so all error handling must account for that case. Maybe one function returns two errors that are meant to be handled in conjunction, like a http status code and the error that caused it, and another function returns multiple errors but certain ones should be handled over overs. There is just no way of knowing without having to read documentation for each function and understand its internal behavior. At that point, what you have is no longer error handling, but a full blown application with business logic, documentation, and various use cases. This is not what we need from our error handling.

One aspect that may not be immediately apparent is that due to golang's rule that functions may not be overloaded, an error type cannot implement both `Unwrap() error` and `Unwrap() []error`. That means that if you have a mix of LinkedList errors and Tree errors you are bound to run into trouble. The error type can only implement one version of that function, so any code out there that type asserts for `Unwrap() error` must now be updated to handle both cases. 

To demonstrate this issue, look at the following example and observe that the result of unwrapping the error is `nil`.
```
// Go Playground: https://go.dev/play/p/0gVlavnmvJf
err := fmt.Errorf("this is bad: %w, %w", err1, err2)
unwrappedErr := errors.Unwrap(err)
fmt.Println(unwrappedErr) // prints <nil>
```
As compared to the same code but only wrapping one error.
```
// Go Playground: https://go.dev/play/p/0gVlavnmvJf
err := fmt.Errorf("this is bad: %w", err1)
unwrappedErr := errors.Unwrap(err)
fmt.Println(unwrappedErr) // prints one
```

When it comes to performance, it just gets worse. We have no ordering to the error tree here, so we still have O(n) at worse case search. Plus, all of the issues that plagued LinkedList errors still plague Tree errors but now the complexities just increased.

## Conclusion and Further Reading

In short, stop. Just do not do it. I guarantee that I have not covered all issues related to Tree errors. There is some great feedback on this approach in various other threads, [like this one](https://www.reddit.com/r/golang/comments/z870te/comment/iyg2o51/?utm_source=share&utm_medium=web2x&context=3). Reading through [the actual proposal thread](https://github.com/golang/go/issues/53435) for this feature illuminates other issues that I had not even considered yet, like thread safety. 

For a straightforward, unbiased post, [this blog post](https://lukas.zapletalovi.com/posts/2022/wrapping-multiple-errors/) explains the new mechanics well. 

In my personal opinion, after having actually removed `error` from an entire application now, we are all better off ignoring `error` completely along with all changes added to errors in go1.20.