Go - Error Handling
Idiomatic error handling is by returning separate error
values. You should return an error variable for problems that can be fixed by the caller.
Error
is an interface that expects type to haveError() string
method- You normally create errors with
errors.New()
andfmt.Errorf()
. - Custom error types must implement the
Error() string
method. - Custom error type names should always start with
Err
. - Error zero value is
nil
and means that no error occurred. - Error variable should be the last returned value in the function signature.
- Error variable names should start with
err
. - Error messages can optionally start with the package name
packagename: lorem ipsum
. - Prefer clarity over grammar in error messages. Message should not start with a forced capital letter and message should not end with a period.
Wrap your errors. Avoid code that panics, return errors. Panics should usually abort the program and errors should usually be handled. Use errors pkg to annotate and wrap those errors.
func getCount(key) (int, error) {
if key <= 0 {
err := errors.New("invalid key")
return 0, err
}
count := 1337
return count, nil
}
// Wrapping errors to get stack trace.
func canProceed(key int) (bool, error) {
count, err := getCount(key)
if err != nil {
// +v prints stack trace as well
// fmt.Printf("%+v", err)
return false, errors.Wrap(err, “getCount failed”)
}
threshold := 1000
return count < threshold, nil
}
Extending Error
with a custom type. Error types should be of the form FooError
. Error values should be of the form ErrFoo
.
package main
import (
"time"
"fmt"
)
// error interface is defined like this:
// type error interface {
// Error() string
// }
// example error
type MyError struct {
When time.Time
What string
}
func (e *MyError) Error() string {
return fmt.Sprintf("at %v, %s", e.When, e.What)
}
func main() {
errMy := MyError{time.Now(), "crashed"}
fmt.Println(errMy)
// => {2009-11-10 23:00:00 +0000 UTC crashed}
}
package main
import (
"errors"
"fmt"
)
// Most native packages follow this error declaration style.
// Allows usage of equality operator `==` to check which error happened.
// Usage of `fmt.Errorf()` will allow more robust error messages
// and custom error types allow extra fields like time stamp,
// but these will make equality checks impossible as Go does not let
// users to redefine struct equality operator.
// It is the best practice to use this approach until you require anything
// more complex.
var (
ErrUserNotFound = errors.New("packagename: user not found")
ErrTimeout = errors.New("packagename: connect timeout")
ErrInvalid = errors.New("packagename: invalid configuration")
)
func getUserName(userId int) (userName string, err error) {
if userId == 0 {
userName = "Ruksi"
} else if userId == 1 {
err = ErrTimeout
} else {
err = ErrUserNotFound
}
return userName, err
}
func main() {
userId := 2 // Try to change this to 0 and 1.
userName, errGet := getUserName(userId)
if errGet == ErrUserNotFound {
fmt.Println("Hmm, seems like user is missing...")
fmt.Println(errGet)
} else if errGet != nil {
fmt.Println("Unknown error!")
fmt.Println(errGet)
} else {
fmt.Println("Found the user!")
fmt.Println(userName)
}
}
Indent error flow. Reduces vertical space the code takes.
// bad
if err != nil {
// error handling
} else {
// normal code
}
// good
if err != nil {
// error handling
return
}
// normal code
Error check can be done inside the condition statement. But it is usually more clean to move the statement outside the conditional.
package main
import (
"fmt"
"strconv"
)
func main() {
// ok
if myInt, err := strconv.Atoi("10"); err != nil {
fmt.Println(err)
return
} else {
fmt.Println(myInt)
}
// better
myIntTwo, errTwo := strconv.Atoi("100");
if errTwo != nil {
fmt.Println(errTwo)
return
}
fmt.Println(myIntTwo)
}
Export errors you want others to handle.
var ErrParse = errors.New("Zero length page name")
func NewPage(name string) (*Page, error)
{
// if it fails...
return nil, ErrParse
}
// if page, err := NewPage("stuff"); err == ErrParse { ... }
Go has panic
s. Panics are as close as exceptions as you get in Go. You rarely catch panics, but it can be done with defer
methods. Panicking shows the call stack and any related running goroutines.
- Panic when something should never happen.
- Panic when you do not want to handle or pass on an
error
. - Panic when the function caller cannot fix the problem, if caller can fix the problem, return an
error
.
package main
import (
"fmt"
"strconv"
)
func doStuff() {
fmt.Println("Hello World!")
}
func main() {
go doStuff()
if myInt, err := strconv.Atoi("non-int"); err != nil {
// Panic because we do not want to handle the error
// and the caller of this function cannot fix the problem.
panic(err) // This shows that `main.doStuff()` is running
} else {
fmt.Println(myInt)
}
}
Report panics. recover
captures the panic value for the current goroutine.
func reportPanics() {
if panic := recover(); panic != nil {
postToSlack(panic)
}
}
func runMyFuncPanicReporting() {
go func() {
defer reportPanics()
myFunc()
}()
}
Wrap your panics. Capture panics in middlewares if possible. Make sure the panic middleware is the first one in the middleware list. panicwrap is useful for wrapping third-party code panics.
func PanicReporterMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
defer reportPanics()
next(w, r)
}
func runServer() {
router := setUpRouter()
n := negroni.New()
n.Use(negroni.HandlerFunc(PanicReporterMiddleware))
n.UseHandler(router)
http.ListenAndServe(":3001", n)
}