How to Avoid Repetitive Error Checking in Go

How to Avoid Repetitive Error Checking in Go

In my last post: Go Error First Impression, I briefly touched on how error handling is unique in Go. To recap:

  • Error is just a value to be returned

  • Error needs to be explicitly checked

We also know this error design comes with a tradeoff: it could lead to dozens of if error checking in real projects, which makes it tedious to write and hard to reason about the normal code execution flow.

Is there a way to avoid this? It turns out we don't need some special techniques. From this Go official blog: Errors are values, we could encapsulate the error just like the way we do to normal values.

The main idea is we don't need to make errors immediately returned for every method call that could go wrong. We can store them as an internal property temporarily, then let the caller check when they eventually need the result. The post gives an example about the usage of bufio package which goes this:

b := bufio.NewWriter()
b.Write(...)
b.Write(...)
b.Write(...)
// and so on
if b.Flush() != nil {
    return b.Flush()
}

As you can see, even though an error could occur during buffer.Write, it does not return as part of the Write method Instead, let the caller do the final check in the Flush when the whole writing finishes. The key takeaway is we do not ignore any error here, just the error checking is abstracted away into Write method.
Any error occurs in any write, the error is stored internally. All subsequent calls to Write method will immediately return if there is an error. The Same in final Flush, it will do nothing if an error already exists. So we do not lose any error checking, it's essentially the same as if we did:

n1, err := b.Write(..)
if err != nil {
  return err
}
n2, err := b.Write(..)
if err != nil {
  return err
}

From the caller's perspective, we still get whatever first occurred error (if it exists) in the final Flush call. But now it's more friendly to write and read.

This works well especially when we have some chainable API, for example, the common object builder pattern. Let's imagine we need to build a complex user object dynamically. We provide chainable API like this:

type UserBuilder struct {
  user model.User
  err  error
}
// how we use the builder to produce the user object
user, err := NewUserBuilder(id).WithAddress().WithBirthday().
 Build()

Let's assume those WithXX methods involve some network I/O to fetch different user info based on the social ID (address, birthday etc.). Obviously, every call involving I/O could go wrong, but the method signature does not need to return error but to store it. Only when the caller tries to get the underlying user object, will the error be returned. Similar idea as bufio package.

In some open-source libraries, we can also find this pattern. For example, in Go ORM library, it uses similar patterns:

res := db.Preload().Select().Find()
if res.Error != nil {
  ...
}

Closing thoughts

errors are values and the full power of the Go programming language is available for processing them.

Quoted from the post again, this is just one scenario to avoid repetitive error checking, where the method consumer calls through multiple methods before hitting the final call (finisher). Each of the methods could yield some errors, but we only need to return it in the finisher method.

Another scenario that leads to repetitive error checking is when we do not have proper encapsulation of our business logic. If we properly abstract away the implementation details, the error checking will be naturally gone. Because if we leak more details, usually it means we call a bunch of low level methods and do error checking along the way. After proper encapsulation, we could just have one coherent method to deal with, hence one error checking will be needed. Inside the encapsulation, it tells the upper caller what the error is. You can check another post from Go blog.

Solid error handling is essential to make robust projects. To treat error as normal values and thoughtfully exposing your method's signature, could help us to achieve a maintainable code without losing any error handling.