David Rio

error defer pattern in go

Before getting in the main topic of this post I wanted to highlight that in go, your error messages should not start with a capitalized letter and should not end with a punctuation of any kind.

Now to our main topics. The code is coming from Jon's book on learning go.

Wrapping errors

Sometimes you want to add additional context to your errors. Maybe you want to display in what function the error happened. We call that wrapping an error. And when you have a series of wrapped errors, then we have an error chain.

We can use the fmt.Errorf function with the special verb %w:

func fileChecker(name string) error {
	f, err := os.Open(name)
	if err != nil {
		return fmt.Errorf("in fileChecker: %w", err)
	}
	f.Close()
	return nil
}

Now from the function that calls fileChecker we can use the errors.Unwrap to log the error in a more informative way:

func main() {
	err := fileChecker("not_here.txt")
	if err != nil {
		fmt.Println(err)
		if wrappedErr := errors.Unwrap(err); wrappedErr != nil {
			fmt.Println(wrappedErr)
		}
	}
}

Using defer to have a more readable code when dealing with errors

I have written quite a few functions that had error code handled similar to this:

func DoSomeThings(val1 int, val2 string) (string, error) {
	val3, err := doThing1(val1)
	if err != nil {
		return "",
			fmt.Errorf("in DoSomeThings: %w", err)
	}
	val4, err := doThing2(val2)
	if err != nil {
		return "", fmt.Errorf("in DoSomeThings: %w", err)
	}
	result, err := doThing3(val3, val4)
	if err != nil {
		return "", fmt.Errorf("in DoSomeThings: %w", err)
	}
	return result, nil
}

But we can do better:

func DoSomeThings(val1 int, val2 string) (_ string, err error) {
	defer func() {
		if err != nil {
			err = fmt.Errorf("in DoSomeThings: %w", err)
		}
	}()
	val3, err := doThing1(val1)
	if err != nil {
		return "", err
	}
	val4, err := doThing2(val2)
	if err != nil {
		return "", err
	}
	return doThing3(val3, val4)
}

Here, we are capturing the error wrapping code in the defer function. All the error checking conditional blocks now simply return the zero value for our return type and the err variable. Before returning from the function, we run the defer function. It is here where we wrap whatever error happen with a helpful string that indicates where in the code the error happened.

Notice how we are using the blank identifier in our function signature. That way we reduce the number of lines in the body of the function without compromising readability. We can do that because we are not referencing that variable anywhere in the code. We know we will generate the final value of that variable right before returning from the function call.