DEV Community

Wesley Powell
Wesley Powell

Posted on

Avoid go1.20 Error Handling

I have already stated just how bad golang error handling is in Golang Error Handling. 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
}

Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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. Reading through the actual proposal thread for this feature illuminates other issues that I had not even considered yet, like thread safety.

For a straightforward, unbiased post, this blog post 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.

Top comments (0)