golang 1.13 errors chain

Abstract

There’s a %w starting from Go 1.13 that can be used in formatter to embed the error:

> fmt.Errorf("failed to execute action one: %w", err)

For a long time, there has been a popular, almost community-wide opinion, that error management in Go is a bit problematic. In this post I’m describing how the things worked in versions 1.12 and earlier, what solutions do 3rd part libraries bring, and what is the suggested approach from Go 1.13 going forward.

Background

Previously in the standard library

Before Go 1.13, there were two ways of dealing with errors in your application. First, was to wrap errors with fmt.Errorf, where you create a new error with the details of the inner one as a part of the message:

func compexAction(input string) (string, error) {
  resultOne, err := simpleActionOne(input) // returns errActionOne
  if err != nil {
    return "", fmt.Errorf("failed to execute action one: %v", err)
  }
  resultTwo, err := simpleActionTwo(resultOne) // returns errActionTwo
  if err != nil {
    return "", fmt.Errorf("failed to execute action two: %v", err)
  }
  return resultTwo, nil
}

That looked fine at the start, but the problems begin when on a higher level you would want to know which error (errActionOne or errActionTwo) was the root cause. To do that, you would need to compare string representation of an error, but binding so tightly on an error message does feel a bit risky (eg if the message changes in the future).

The second approach would be to pass raw errors up to the caller, and then compare the exact type returned from complexAction:

func compexAction(input string) (string, error) {
  resultOne, err := simpleActionOne(input) // returns errActionOne
  if err != nil {
    return "", err // returns errActionOne as well
  }
  resultTwo, err := simpleActionTwo(resultOne) // returns errActionTwo
  if err != nil {
    return "", err // returns errActionTwo as well
  }
  return resultTwo, nil
}

The issue here is that you need to remember that the error is not modified at any level on your app between the root cause and the top-level error checking. This is very risky, as it’s easy to forget this restriction, and hard to track when that promise is broken. Another issue is that debugging is much, much harder - it’s hard to track the execution flow, especially if two or more paths could end with the same error (eg. SQL’s unique constraint error).

Because these two approaches are not ideal, many Go developers went searching for answers to the community libraries.

Community response - pkg/errors

Probably the most popular solution to this problem is pkg/errors created by Dave Cheney. Searching for the name of the library at Github returns over 677k results, and this includes only the public repositories! What made it an obvious choice for me was its simplicity, as you only need to use two functions: Wrap (optionally Wrapf) to add context to an error, and Cause() extract the root cause:

import "github.com/pkg/errors"
 
func compexAction(input string) (string, error) {
  if err != nil {
    return "", errors.Wrap(err, "failed to execute action one")
  }
  resultTwo, err := simpleActionTwo(resultOne) // returns errActionTwo
  if err != nil {
    return "", errors.Wrap(err, "failed to execute action two")
  }
  return resultTwo, nil
}

While this helps us with the issues that the standard library had, there is one small disadvantage with using pkg/error: when the error is logged with its stack trace, it includes the line where error type is defined (the one with var errSomeIssue = errors.New("some error")) is included. Not a huge issue, but still something that you need to take into consideration when using the library. To avoid that, you can define the errors using either standard library errors or fmt.Errorf(..), but then use pkg/errors exclusively.

Current solution

The annual Go Survey revealed that one of the biggest issues in the language is error handling. This is why the core team focused on this problem and came back with a solution in 1.13 release. The change is so similar to pkg/errors package, that many people (including me) were wondering why they didn’t just include the library in the language, rather than creating something of their own.

Again, you need to remember about two methods: wrapping an error with some additional information is done, as previously, via fmt.Errorf(..), but this time we are equipped with a new formatting verb (a place in the format string that is later replaced by some value passed in as an argument) - %w. Whenever you pass an error to that place, the result fmt.Errorf will embed that error, so that you can, later on, check if that was indeed the root cause. To check that, you use errors.Is(returnedErr, rootErrType) that returns boolean value.

func compexAction(input string) (string, error) {
  resultOne, err := simpleActionOne(input) // returns errActionOne
  if err != nil {
    return "", fmt.Errorf("failed to execute action one: %w", err)
  }
  resultTwo, err := simpleActionTwo(resultOne) // returns errActionTwo
  if err != nil {
    return "", fmt.Errorf("failed to execute action two: %w", err)
  }
  return resultTwo, nil
}

Another function that was added to errors in Go 1.13 is errors.As is a shortcut for casting the complex error to some other type, so that you have access to the specific fields/methods of the root cause error, which would be otherwise not accessible due to the limitation of errors interface:

isRootErr := errors.As(returnedErr, &myRootErr{})

This way you can both check if the type matches, and get access to the specific methods/fields.

While I really like that the Go team introduces an ability to wrap errors with additional context, I see some issues in this solution. First thing I did not like was the way we wrap error, because fmt.Error(..) looks a bit odd, especially with that ugly formatting verb. I can accept this, though, as I take “one way to do things” over my personal preference. What I cannot accept is that the standard library errors wrapping is based on Unwrap() error method on error interface, while pkg/errors uses Cause() error function. This will cause a lot of project to hesitate before switching over to the new approach, as the unwrapping will not work as long as the libraries stick to pkg/errors. There is a Github issue related to this, but it’s something that the library needs to add (which does not solve the problem in 100%), and the change has not been merged yet, either.

Summary

There are both good and bad things about the solution introduced to errors in Go 1.13, but it’s a step in the right direction. I would recommend using it for all new libraries you are going to develop and start transforming the ones you created already (make sure you bump the major version though, as it introduces a breaking change). I’m not sure about switching to it in services unless you are sure your dependencies wrap errors in a consistent way.