Pocket Gophers

Why You Should Not Use checkErr

First off, what is checkErr? It’s a function that is meant to stop the process when an error is encountered. Two possible definitions are:

func checkErr(err error) {
	if err != nil {
		panic(err.Error())
	}
}

And:

func checkErr(err error) {
	if err != nil {
		log.Fatal(err)
	}
}

The purpose of checkErr is so that you don’t repeat yourself (DRY) when handling errors. Take, for example, my method for error checking while prototyping, which tells you to initially handle every error with:

if err != nil {
	log.Fatalln(err)
}

Compare that with checkErr:

checkErr(err)

That saves you two lines every time an error is checked. As an implementation of checkErr is about five lines, you only need to handle three errors to start saving lines of code. Less code is better, right?

I think the cost of writing a little boilerplate far outweigh the problems and limitations of checkErr, some of which are explained below.

Badly named

From its name, checkErr, I can’t tell what the error is checked for or what happens when the error matches. For example, does checkErr panic or call os.Exit (via log.Fatal, etc.)? This is important to know because the program can recover from panic, but not from os.Exit.

A checkErr that panics can be used in an http.Handler to stop the request, but not cause the entire server to stop. This is because http.Server recovers from panic. If checkErr calls os.Exit, there is nothing the server can do to keep running.

A better name is panicOnErr or fatalOnErr (depending on your implementation). These name tells me the control flow ( panic or fatal) and the condition (existence) the error is checked for.

Can only panic or os.Exit

Consider this trivial function:

func foo() {
	for i := 0; i < 10; i++ {
		err := bar(i)
		checkErr(err)
	}
}

If bar returns an error, Go provides the following control flow options:

Of these, only panic and os.Exit can be used by checkErr because all the other options can only be used inside of foo itself. For example, calling return in checkErr returns from checkErr back to foo, where execution resumes with the next loop iteration.

If you need to return, break, goto, continue, if, or for when there is an error, you have to write an if err != nil {} block. Instead of DRYing your code, checkErr

Don’t panic

Defer, Panic, and Recover and PanicAndRecover describe how to recover from a panic.

Don't panic is a Go Proverb because of the limited things that can be done when recovering. Since recover only works in a defered function, you face the same limited control flow options as checkErr does because it uses the called function’s scope, with one addition: the possibility of assigning the values returned by the function defer was called in.

Returning errors is much more flexible.

Breaks the pattern

The general pattern for error handling looks like:

result, err := DoSomething()
if err != nil {
	// handle the error
}

Or:

err := SomethingElse()
if err != nil {
	// handle the error
}

Since it is used so often, it becomes familiar and therefore easy to find and/or skip over. The entire if condition doesn’t need to be read to see that it is for error handling. You get that from if err.

To see if a returned error is handled, you only need to glance at the beginning of the next line. Since the error handling code is indented, finding the end of that code is simple.

checkErr is an exception to that pattern. Instead of just looking for if err (or even the whitespace caused by the indented error handling code), you also need to look for checkErr.

This is the same reason I don’t like:

if err := MoreWork(another); err != nil {
    return nil, err
}

While they are more terse, they also make the code harder to scan.

Boilerplate

My initial error handling code takes the form of:

if err != nil {
	log.Fatalln(err)
}

Or:

if err != nil {
	return nil, err
}

The key word here is initial. As soon as you need to customize the error handling, you need the if block.

My boilerplate doesn’t stay untouched

Code boilerplate: Is it always bad?

The actual boilerplate for the initial code is:

if err != nil {
}

It is two lines and trivial. The interior of that if block is almost always custom. For example, the number and type of arguments that are returned varies with the function they are returned from.

What about functions like os.IsExist?

The problem with checkErr isn’t that you used a function to check an error. The problem is that checkErr also tries to affect the control flow, which it can only do with panic and os.Exit.

Functions like os.IsExist don’t try to affect control flow. They check error conditions. In this case, to see if the error is one of the many errors that could happen when a file already exists.

Dig into the Fundamentals of Go

Subscribe to receive a weekly email covering a Go fundamental. Be it the language, its tooling, or its packages, you will learn what you need to know.