The problem with Error handling in Go

Bob Fanger
5 min readFeb 23, 2018

--

The problem I have with errors in golang is two-fold, it’s unpredictable and it’s very verbose

Unpredictable

Let’s take the following code for example:

func main()  {    fmt.Println("starting...")    pkg.DoSomeWork()    fmt.Println("done")}

You can’t tell what’s wrong with this code unless you lookup the definition of the DoSomeWork() function:

func DoSomeWork() error {
return errors.New(“Useful information about what went wrong”)
}

So the when DoSomeWork() encounters an scenario which it can’t handle and it returns an error, that error is ignored and the program behaves like it did work.

As you’d probably know, the correct code should have been:

func main() {
fmt.Println(“starting…”)
if err := pkg.DoSomeWork(); err != nil {
panic(err)
}
fmt.Println(“done”)
}

And you know this code pattern well, because in programming a lot can go wrong so in Go you’ll write this code over and over.

So much can go wrong in programming, that I sometimes think to myself, “How is it possible that anything works at all.”

Which takes me the the second issue with errors in go

Very verbose

if err := pkg.DoSomeWork(); err != nil {
return err // or panic(err)
}

But when the function also return a value, you’ll either need to write:

value, err := pkg.SomeValue()
if err != nil {
return err
}

or

var value *pkg.MyValueType
if value, err := pkg.SomeValue(); err != nil {
return err
}

It’s so bad, that most examples, tutorials and livecoding sessions contain code like this:

value, _ := pkg.SomeValue()

Followed by an explanation that you shouldn’t write code like this for production.

Go doesn’t let me write the code I wanted to write

Some attempts have been made to combat the verbosity:

Must*()

regexp.MustCompile() for example is the same as regex.Compile() except that it panics when it encounters an error.

This should only be used in exceptional situations where you know at compile time it’s going to work. If you use a hardcoded valid regex it would never error.

ErrorCollector types

type SafeDoSomeWork struct {
Err error
}
func (s *SafeDoSomeWork) DoSomeWork() {
if (s.Err) {
return
}
s.Err = DoSomeWork()
}
func main() {
safe := SafeWorker()
safe.DoSomeWork()
safe.DoSomeWork()
safe.DoSomeWork()
if (safe.Err) {
panic(safe.Err)
}
}

But this do-nothing-when-an-error-occurred is still a lot of code and still prone to unhandled errors.

Summary of the problem

The good

If you want/can handle the error, the error as a returning value is an very elegant solution.

The bad

If you can’t the handle error, you end writing a lot of if err != nil code.

The ugly

If you don’t handle the error (either accidental or in a hurry), you’ll write an unstable, unpredictable program, with weird, hard-to-debug problems.

New Proposal

The Go 2 proposal adds check/handle syntax which looks promising at solving the verbosity issue.

check pkg.DoSomeWork()

For the predicability issue i propose adding a linting rule that detects if an error is not being handled.

I’m not sure it it should also generate a linting error when it happens inside a defer block, it’s probably not needed as these are generally used for cleanup / freeing up resources.

Ignoring an unhandled error would be it to assign it to_ :

_ := pkg.DoSomeWork()

as _ is used in other places for ignoring stuff too.

Tip: Augment errors with context

Instead of returning errors as-is from your function use github.com/pkg/errors (or a similar error package) to add context:

func DoSomeWork() error {
if err := thing.SomeNetworkOperation(); err != nil {
return errors.Wrap(err, “network operation failed”)
}
return nil
}

Error: somework failed: network operation failed: length was 0
instead of
Error: length was 0

The first makes debugging an troubleshooting much easier.

The (older) Proposal

I started using https://github.com/pkg/errors which adds the ability to augment errors with useful information. This proposal doesn’t have that feature and encourages using too short errors messages

Let’s look at the same code as in the beginning.

func main() {
pkg.DoSomeWork()
}

The new compiler sees a call to a function that returns an error which isn’t handled and throw a compiler error.

When we add a special keyword panic

func main() panic {
pkg.DoSomeWork()
}

The code will compile and behave as if we’d written:

func main() {
if err: = pkg.DoSomeWork(); err != nil {
panic(error)
}
}

With values (multiple return):

func main() {
value := pkg.SomeValue()
}

The compiles sees that SomeValue() returns a value and an error and complain as usual with “multiple-value SomeValue() in single-value context”

But when we add the panic keyword the code would compile as if we’d written:

func main() {
value, err := pkg.SomeValue()
if (err != nil) {
panic(err)
}
}

That covers the panic scenario, but we don’t want to crash our program that often, in a lot of cases we can handle the error or pass it upstream.

func DoSomeWork() error {
i := strconv.Atoi("-42")
}

The new compiler sees the function can return an error an compiles as if we’d written:

func DoSomeWork() error {
var err error
i, err := strconv.Atoi("-42")
if err != nil {
return err
}
return nil
}

Functions can also return both a value and error (but we don’t reason about it that way, i read it as “value or error”), take the following code:

func SomeValue() (*MyValueType, error) {
i := strconv.Atoi("-42")
return &MyValueType{ Value: i}
}

When it encounters a unhandled error it returns the error and the current or zero value of other return values.

And instead of “not enough arguments to return” the compiler sees that the last type of the return values is an error and returns the value and nil as error.

As if you’d written:

func SomeValue() (*MyValueType, error) {
i, err := strconv.Atoi(“-42”)
if err != nil {
return nil, err
}
return &MyValueType{Value: i}, nil
}

Summary of the proposal

1. If you want to handle in a specific way, you can handle it like you’ve always done.

2. If you don’t handle the error the compiler doesn’t compile, you’ll need to explicitly ignore the error _ := DoSomeWork()

3. If you don’t write the error handling code the compiler writes it for you in a predicable way.

4. The language reduces error handling boilerplate and let’s you write clean readable code.

To the language implementers

I know this is a breaking change to the language, and if you use semver would result in Go 2.0

Some or the features can be retrofitted into 1.x but please don’t do this, this would pollute the language for years to come. I believe elegant, predictable errorhandling is a killer-feature worth upgrading for.

And don’t try to do to much for 2.x either, that could delay a release for years, leave that for a 3.0

--

--