Golang: Error Handling In One Line of Code
The last year I wrote a lot of Golang code in small and mid size projects. I like to share my experience on error handling and introduce some helper functions which help me to write less verbose code. You will learn how to add better context to your errors and get around the idiomatic but verbose error handling pattern: Check, Log and Return.
value, err := DoSomething()
if err != nil { // Check
log.Println(err) // Log (optional)
return err // Return
}
This post is based and influenced by the errors pkg and Blog posts by Dave Chaney — error-handling and dont-just-check-errors-handle-them-gracefully
Problems to Address
- Verbosity, when checking for errors we usually write 3-4 lines of code. This can easily blows up our code base with a lot of error handling code making it harder to read.
- Context, in a deep call stack we have no idea about the origin of the error if it bubbles up to the calling function. (Did you ever copy & pasted a error message to search for it in a whole project?)
- Memory Corruption, these errors are hard to find and debug
Verbosity & Context
To get rid of the verbose Check and Log pattern (Return follows later), we can write a function to do this for us.
//Log checks and logs a error
func Log(err error) {
if err != nil {
log.Printf("%+v", errors.Wrap(err))
}
}
errors.Wrap adds a stack trace to the error. It can be used like this:
func CalculateAnswer(question string) ( string, error ) {
if strings.compare(question, "") == 0 {
return num, errors.New("Please ask a valid question")
}
var answer string
//... Fancy calculation ... return answer, nil
}answer, err := CalculateAnswer("")
Log(err)
Let’s have a look at our error msg.
2017/12/28 17:00:01 Please ask a valid questiongoerr.TestCommonError
/home/equanox/go/src/errz/error_test.go:96
testing.tRunner
/usr/local/go/src/testing/testing.go:746
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:2337
There it is. The error message is preserved and we have added context through a stack trace to the error msg. With a decent IDE or TextEditor you can click the error msg and directly jump to the line where it occurred.
Nice. We can now Check, Log and add Context to our errors with one line of code. This removes verbosity and makes our code more readable. In the next section I will show you how we can Check and Return in one line of code
Returning an Error & Detecting Memory Corruption
Let’s say we want to keep the api of our simple Log function and add the possibility to return a error. This means ending the executing of the current function in place and resume action in the calling function. In languages like
C++ or Java there is a built in mechanism called Exceptions to handle this. In Golang we don’t have exceptions but can get around the problem making clever use of go’s panic/recover mechanism and its feature of named return values.
Let’s have a look at our example:
func CalculateAnswer(question string) ( value int, err error ) {
valid, err = CheckQuestion(question)
Log(err) var answer string
//... Fancy calculation ... return answer, err
}
When CheckQuestion
returns a error we want CalculateAnswer
return immediately without a extra error check if err != nil { return "", err}
after Log(err)
. We also want to preserve the error msg returned by CheckQuestion
. This can be done by a panic inside Log
.
When a panic happens…
the process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes. From defer-panic-and-recover
Cause we do not want the panic to end the program, we gonna recover from the panic before we leave the scope of CalculateAnswer
. This can be done using a combination of defer
and recover
. From a user perspective handling intended panics would lead to more verbose code and even more errors. We do not want our panic to leave the scope, we are in control of. Keep in mind..
Never panic across package boundaries
The function Fatal
will Check and panic:
func Fatal(err error) {
if err != nil {
panic(errors.Wrap(err))
}
}
To handle this panic we need another helper function called Recover(errs …*error)
. So we can recover from a panic and add a stack trace to the error and write its value to a error pointer, which points to our named return value err. When no error pointer is passed it just Logs the error. You can skip the implementation detail of Recover()
if you want but I leave it here if your interested.
func Recover(errs ...*error) {
var e *error
for _, err := range errs {
e = err
break
} //handle panic
if r := recover(); r != nil {
var errmsg error
//Preserve error which might have happened before panic/recover
if e != nil && *e != nil {
errmsg = errors.Wrap(*e, r.(error).Error())
} else {
//No error occurred just add a stacktrace
errmsg = errors.Wrap(r.(error), "")
} //If error can't bubble up -> Log it
if e != nil {
*e = errmsg
} else {
log.Printf("%+v", errmsg)
}
}
}
Here is a example how it can be used together:
func CalculateAnswer(question string) ( value int, err error ) {
defer Recover(&err)
valid, err = CheckQuestion(question)
Fatal(err)//Do calculation
value = 42 return value, err
}
Isn’t it nice? We can now Check and Return with one line of code (+ 1 line at the top). As a nice extra, Recover()
gives us hints where a memory corruption has happened through a stack trace.
I bundled these functions in a package called errz. Feel free to use it.
Do i need a error message when i have a stack trace?
In brief: Yes
Having a error message for your future self or any other developer will improve your code quality. Stack traces can help to track down the error but human readable error message can give even more context and make it more pleasant for anyone who needs to handle your errors.
Performance
I did some research on the go compiler ability to inline a function call. Though, i have to admit it’s ability is rather limited, but simple to understand.
Only short and simple functions are inlined. To be inlined a function must contain less than ~40 expressions and does not contain complex things like function calls, loops, labels, closures,
panic
's,recover
's,select
's,switch
'es, etc.
This means the functions Log()
, Fatal()
and Recover()
from the errz package are no candidates for inlining at the moment.
The state we are interested in, is when our application runs as intended and no error happens. This brings our benchmarks down to checking the error and resume normal program execution. I did the benchmarks on a function using different types of error handling, while LogRaw means the most pure check if err != nil {}
BenchmarkCheckLogRaw-4 1000000000 2.42 ns/op
BenchmarkCheckLog-4 300000000 4.25 ns/op //Using errz.Log
BenchmarkCheckFatal-4 30000000 58.40 ns/op //Using errz.Fatal
You see, Log
increases computing time by factor ~2 and Fatal by factor ~24 . The main reason for CheckFatal being so slow is the use of defer Recover(&err)
which adds nearly 50 ns.
//errz.Fatal without defer
BenchmarkCheckFatal-4 200000000 9.06 ns/op
Without defer
computing overhead is only factor ~4 . There is a ongoing disucusion on the speed of defer
https://github.com/golang/go/issues/14939
Conclusion
I showed you a possibility to bring down error handling to three functions Log
, Fatal
and Recover
and reduce the verbosity of error handling to a minimum. These functions can be used to give more context to errors through stack traces. The relative performance overhead of using Fatal
in combination with Recover
is significant but mostly related to the usage of defer
. Ask yourself how often you decided not to use defer because of performance reasons? A overhead of ~60ns is fine for most application and will likely go down in newer versions of Golang.
You can go get
these functions in a package called errz. Or use dep to depend on the v0.1 release.
Feel free to comment on this or send pull requests.