Avoid checking if error is nil repetition?

Go

Go Problem Overview


I'm currently learning go and some of my code looks like this:

a, err := doA()
if err != nil {
  return nil, err
}
b, err := doB(a)
if err != nil {
  return nil, err
}
c, err := doC(b)
if err != nil {
  return nil, err
}
... and so on ...

This looks kinda wrong to me because the error checking takes most of the lines. Is there a better way to do error handling? Can I maybe avoid this with some refactoring?

UPDATE: Thank you for all the answers. Please note that in my example doB depends on a, doC depends on b and so on. Therefore most suggested refactorings don't work in this case. Any other suggestion?

Go Solutions


Solution 1 - Go

This is a common complaint, and there are several answers to it.

Here are a few common ones:

1 - It's not so bad

This is a very common reaction to these complaints. The fact you have a few extra lines of code in your code is not in fact so bad. It's just a bit of cheap typing, and very easy to handle when on the reading side.

2 - It's actually a good thing

This is based on the fact that typing and reading these extra lines is a very good reminder that in fact your logic might escape at that point, and you have to undo any resource management that you've put in place in the lines preceding it. This is usually brought up in comparison with exceptions, which can break the flow of logic in an implicit way, forcing the developer to always have the hidden error path in mind instead. Some time ago I wrote a more in-depth rant about this here.

3 - Use panic/recover

In some specific circumstances, you may avoid some of that work by using panic with a known type, and then using recover right before your package code goes out into the world, transforming it into a proper error and returning that instead. This technique is seen most commonly to unroll recursive logic such as (un)marshalers.

I personally try hard to not abuse this too much, because I correlate more closely with points 1 and 2.

4 - Reorganize the code a bit

In some circumstances, you can reorganize the logic slightly to avoid the repetition.

As a trivial example, this:

err := doA()
if err != nil {
    return err
}
err := doB()
if err != nil {
    return err
}
return nil

can also be organized as:

err := doA()
if err != nil {
    return err
}
return doB()

5 - Use named results

Some people use named results to strip out the err variable from the return statement. I'd recommend against doing that, though, because it saves very little, reduces the clarity of the code, and makes the logic prone to subtle issues when one or more results get defined before the bail-out return statement.

6 - Use the statement before the if condition

As Tom Wilde well reminded in the comment below, if statements in Go accept a simple statement before the condition. So you can do this:

if err := doA(); err != nil {
    return err
}

This is a fine Go idiom, and used often.

In some specific cases, I prefer to avoid embedding the statement in this fashion just to make it stand on its own for clarity purposes, but this is a subtle and personal thing.

Solution 2 - Go

You could use named return parameters to shorten things a bit

Playground link

func doStuff() (result string, err error) {
	a, err := doA()
	if err != nil {
		return
	}
	b, err := doB(a)
	if err != nil {
		return
	}
	result, err = doC(b)
	if err != nil {
		return
	}
	return
}

After you've been programming in Go a while you'll appreciate that having to check the error for every function makes you think about what it actually means if that function goes wrong and how you should be dealing with it.

Solution 3 - Go

If you have many of such re-occurring situations where you have several of these error checks you may define yourself a utility function like the following:

func validError(errs ...error) error {
	for i, _ := range errs {
		if errs[i] != nil {
			return errs[i]
		}
	}
	return nil
}

This enables you to select one of the errors and return if there is one which is non-nil.

Example usage (full version on play):

x, err1 := doSomething(2)
y, err2 := doSomething(3)

if e := validError(err1, err2); e != nil {
    return e
}

Of course, this can be only applied if the functions do not depend on each other but this is a general precondition of summarizing error handling.

Solution 4 - Go

You could create context type with result value and error.

type Type1 struct {
	a int
	b int
	c int

	err error
}

func (t *Type1) doA() {
	if t.err != nil {
		return
	}

	// do something
	if err := do(); err != nil {
		t.err = err
	}
}

func (t *Type1) doB() {
	if t.err != nil {
		return
	}

	// do something
	b, err := t.doWithA(a)
	if err != nil {
		t.err = err
		return
	}

	t.b = b
}

func (t *Type1) doC() {
	if t.err != nil {
		return
	}

	// do something
	c, err := do()
	if err != nil {
		t.err = err
		return
	}

	t.c = c
}

func main() {

	t := Type1{}
	t.doA()
	t.doB()
	t.doC()

	if t.err != nil {
		// handle error in t
	}

}

Solution 5 - Go

It looks wrong to you perhaps because you are used to not handling errors at the call site. This is quite idiomatic for go but looks like a lot of boilerplate if you aren't used to it.

It does come with some advantages though.

  1. you have to think about what the proper way to handle this error is at the site where the error was generated.
  2. It's easy reading the code to see every point at which the code will abort and return early.

If it really bugs you you can get creative with for loops and anonymous functions but that often gets complicated and hard to read.

Solution 6 - Go

You can pass an error as a function argument

func doA() (A, error) {
...
}
func doB(a A, err error)  (B, error) {
...
} 

c, err := doB(doA())

I've noticed some methods in the "html/template" package do this e.g.

func Must(t *Template, err error) *Template {
	if err != nil {
		panic(err)
	}
	return t
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBajuView Question on Stackoverflow
Solution 1 - GoGustavo NiemeyerView Answer on Stackoverflow
Solution 2 - GoNick Craig-WoodView Answer on Stackoverflow
Solution 3 - GonemoView Answer on Stackoverflow
Solution 4 - GozouyingView Answer on Stackoverflow
Solution 5 - GoJeremy WallView Answer on Stackoverflow
Solution 6 - Gorobert kingView Answer on Stackoverflow