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:
panicos.Exitreturnbreakgotocontinueiffor
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.