Error Handling in Go that Every Beginner should Know
Go doesn’t have exceptions, so it doesn’t have try…catch or anything similar. How can we handle errors in Go then?
There are two common methods for handling errors in Go — Multiple return values and panic.
Error Handling using Multiple Return Values
We can take advantage of multiple return values feature by adding an error struct to returned values. By convention, an error value will be on the right and a result value will be on the left. This convention kind of bugs me because it doesn’t sound right that the error is on the right side :D.
func Marshal(v interface{}) ([]byte, error) {
e := &encodeState{}
err := e.marshal(v, encOpts{escapeHTML: true})
if err != nil {
return nil, err
}
return e.Bytes(), nil
}
The above code snippet is taken from the json package in the standard library. Notice that the error is a type. It is an interface declared in the builtin package.
type error interface {
Error() string
}
The problem of this error handling pattern is that there is no enforcement from a compiler. It is up to you on what and how your function returns an error. You can put an error struct in any position of the returned values. It can be on the left or it can be in the middle if you have more than two returned values. Actually, it doesn’t have to be an error type at all. It’s all up to you. However, you may want to follow the standard convention if you don’t want your colleagues to come to your desk and ask you to change (forcefully).
Note that the error interface contains one function which is Error() string. There is no short way to create an anonymous type in Go. You have to define a struct and use method sets to associate a function to your struct.
// Define your Error struct
type MyError struct {
msg string
}
// Create a function Error() string and associate it to the struct.
func (error *MyError) Error() string {
return error.msg
}
// Now you can construct an error object using MyError struct.
func ThisFunctionReturnError() error {
return &MyError{"custom error"}
}
This code looks very tedious. Okay, you can create a common package and share this piece of code from there. Fortunately, the standard library’s got you covered. There is the errorString struct defined in the errors package.
Stack Traces
We can refactor our previous code to use the built-in error struct as follows.
import "errors"func ThisFunctionReturnError() error {
return errors.New("custom error")
}
It now looks much better, doesn’t it? You may still have a question about stack traces based on the error interface you have seen . Unfortunately, the standard errors does not come with stack traces. Without stack traces, it is very difficult to debug when you have no idea where the error occurs. The error could pass through 10 function calls before it gets to the code that prints it out. Many people have this exact same problem and they created awesome projects to handle this issue. palantir/stacktrace, go-erros/errors, and pkg/errors are one of them.
In the future, Go standard library may come with the richer error type and it could render all of those libraries obsolete. But, for now, pick one of them because you definitely need it. I personally like pkg/errors the most since it’s compatible with the standard package. Basically, you can just change the import statement from “errors” to “github.com/pkg/errors” and call it a day.
import (
"github.com/pkg/errors"
"fmt"
)
func A() error {
return errors.New("NullPointerException")
}
func B() error {
return A()
}
func main() {
fmt.Printf("Error: %+v", B())
}
If you want to print stack traces instead of a plain error message, you have to use %+v instead of %v in the format pattern, and the stack traces will look similar as below. This library has many other functions that are very useful, but be careful that those functions are not defined in the standard package. You’re creating a library lock-in when you use other functions besides errors.New.
Error: NullPointerException
main.A
/Users/hussachai/Go/src/awesomeProject/error_handling.go:9
main.B
/Users/hussachai/Go/src/awesomeProject/error_handling.go:13
main.main
/Users/hussachai/Go/src/awesomeProject/error_handling.go:17
runtime.main
/usr/local/opt/go/libexec/src/runtime/proc.go:198
runtime.goexit
/usr/local/opt/go/libexec/src/runtime/asm_amd64.s:2361
The Ugly Error Checking
When you use multiple return values to handle errors, eventually you may end up with a deeply nested if-block which is one of the pain point of this error handling style.
func EatOrange() (*C, error) {
var err error
var a string
a, err = GetA()
if err == nil {
var b string
b, err = GetB(a)
if err == nil {
var c string
c, err = GetC(b)
if err == nil {
return c, nil
}
}
}
return nil, err
}
The levels of nesting can go terribly deeper than the example above. Composing a decision tree using the if-control-flow is pretty ugly IMO, yet it is surely easy to understand. This is a trade off that you have to balance it out while you’re crafting your code.
Opting Out an Error
In Go, you can omit one or more variable assignments by putting an underscore on the position of the variable you want to omit on the left-hand-side. The underscore in this context is called blank identifier. Since an error is a plain struct, you can apply the blank identifier on them as other returned values.
result, _ := EatOrange() // omit the error assignment
_, err := EatOrange() // omit the result struct
Defer, Panic, and Recover
Here you go. The one resembling an exception that you’re familiar with. I mean, you can break the flow by throwing it (panic). Go has a pretty unique way to handle the panicking (exception).
Go’s blog covers this topic very well (of course). If you are feeling lazy and want to know how it works quickly, please read on.
Defer is a language mechanism that puts your function call into a stack. The deferred functions will be executed in reverse order when the host function finishes regardless of whether a panic is called or not. The defer mechanism is very useful for cleaning up resources.
resp, _ := http.Get("http://golang.org")
defer func () {
if resp != nil {
resp.Body.Close()
}
}()
body, _ := ioutil.ReadAll(resp.Body)
Panic is a built-in function that stops the normal execution flow. The deferred functions are still run as usual.
func panic(v interface{})
See how it works in action: https://play.golang.org/p/sfkGfBo04d3
When you call panic and you don’t handle it, the execution flow stops, all deferred functions are executed in reverse order, and stack traces are printed at the end.
You can pass any types into the panic function. However, I’d recommend passing an error struct because you will not lose stack traces when you recover a panic. Of course, you have to use one of those errors libraries I mentioned earlier.
Recover is a built-in function that returns the value passing from a panic call. This function must be called in a deferred function. Otherwise, it always returns nil.
package main
import (
"fmt"
"github.com/pkg/errors"
)
func A() {
defer fmt.Println("A")
defer func() {
if r := recover(); r != nil {
fmt.Printf("Panic: %+v\n", r)
}
}()
B()
}
func B() {
defer fmt.Println("B")
C()
}
func C() {
defer fmt.Println("C")
Break()
}
func Break() {
defer fmt.Println("D")
panic(errors.New("the show must go on"))
}
func main() {
A()
}
Converting Panicking into a Returned Error.
Sometimes, you don’t want to stop the whole execution flow due to a panic, but you want to report an error back to a caller as a returned value. In this case, you have to recover a panicking goroutine and grab an error struct obtaining from the recover function, and then pass it to a variable. It’s not difficult as it sounds, and that is how named returned values come into play.
func Perform() (err error) {
defer func() {
if r := recover(); r != nil {
err = r.(error)
}
}()
GoesWrong()
return
}
func GoesWrong() {
panic(errors.New("Fail"))
}
func main() {
err := Perform()
fmt.Println(err)
}
The deferred functions will be executed after a function call, but before a return statement. So, you have an opportunity to set a returned variable before a return statement gets executed.
I hope this article is useful to you. Please let me know if you have any comments or suggestions. Thank you!