Error Types

We prefer the use of the CockroachDB errors library at github.com/cockroachdb/errors. This is a superset of Go's own errors and pkg/errors.

When returning errors, consider the following to determine the best choice:

If the client needs to detect the error, and you have created a simple error using errors.New, use a var for the error.

Bad

Good

// package foo

func Open() error {
  return errors.New("could not open")
}

// package bar

  if err := foo.Open(); err != nil {
    if err.Error() == "could not open" {
      // handle
    } else {
      panic("unknown error")
    }
  }
// package foo

var ErrCouldNotOpen = errors.New("could not open")

func Open() error {
  return ErrCouldNotOpen
}

// package bar

if err := foo.Open(); err != nil {
  if errors.Is(err, foo.ErrCouldNotOpen) {
    // handle
  } else {
    return errors.Wrap(err, "unknown error")
  }
}

If you have an error that clients may need to detect, and you would like to add more information to it (e.g., it is not a static string), then you should use a custom type.

Bad

Good

func open(file string) error {
  return fmt.Errorf("file %q not found", file)
}

func use() error {
  if err := open(); err != nil {
    if strings.Contains(err.Error(), "not found") {
      // handle
    } else {
      panic("unknown error")
    }
  }
}
type errNotFound struct {
  file string
}

func (e errNotFound) Error() string {
  return fmt.Sprintf("file %q not found", e.file)
}

func open(file string) error {
  return errNotFound{file: file}
}

func use() error {
  if err := open(); err != nil {
    var nfErr errNotFound
    if errors.As(err, &nfErr); ok {
      // handle
    } else {
      return errors.Wrap(err, "opening file")
    }
  }
}

Error Wrapping

There are three main options for propagating errors if a call fails:

It is recommended to add context where possible so that instead of a vague error such as "connection refused", you get more useful errors such as "call service foo: connection refused".

When adding context to returned errors, keep the context succinct by avoiding phrases like "failed to", which state the obvious and pile up as the error percolates up through the stack:

Bad

Good

s, err := store.New()
if err != nil {
    return errors.Newf(
        "failed to create new store: %s", err)
}
s, err := store.New()
if err != nil {
    return errors.Wrap(
        "new store", err)
}
failed to x: failed to y: failed to create new store: the error
x: y: new store: the error

However once the error is sent to another system, it should be clear the message is an error (e.g. an err tag or "Failed" prefix in logs).

See also Don't just check errors, handle them gracefully — but be mindful that Go has evolved since this article was written, and use errors.Is() and errors.As() where applicable.

Handle Type Assertion Failures

The single return value form of a type assertion will panic on an incorrect type. Therefore, always use the "comma ok" idiom.

Bad

Good

t := i.(string)
t, ok := i.(string)
if !ok {
  // handle the error gracefully
}

Don't Panic

Code running in production must avoid panics. Panics are a major source of cascading failures. If an error occurs, the function must return an error and allow the caller to decide how to handle it.

Bad

Good

func foo(bar string) {
  if len(bar) == 0 {
    panic("bar must not be empty")
  }
  // ...
}

func main() {
  if len(os.Args) != 2 {
    fmt.Println("USAGE: foo <bar>")
    os.Exit(1)
  }
  foo(os.Args[1])
}
func foo(bar string) error {
  if len(bar) == 0
    return errors.New("bar must not be empty")
  }
  // ...
  return nil
}

func main() {
  if len(os.Args) != 2 {
    fmt.Println("USAGE: foo <bar>")
    os.Exit(1)
  }
  if err := foo(os.Args[1]); err != nil {
    panic(err)
  }
}

Panic/recover is, in most cases, not an error handling strategy. A program must panic only when something irrecoverable happens such as a nil dereference.

An exception to this is program initialization: bad things at program startup that should abort the program may cause panic.

var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))

Another exception is when a package is purely functional and has no side effects. For example, parts of SQL planning in CockroachDB uses panic propagation for error handling in that case.

Even in tests, prefer t.Fatal or t.FailNow over panics to ensure that the test is marked as failed.

Bad

Good

// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
  panic("failed to set up test")
}
// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
  t.Fatal("failed to set up test")
}