ruk·si

🐹 Go
Error Handling

Updated at 2018-12-31 05:17

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 have Error() string method
  • You normally create errors with errors.New() and fmt.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 panics. 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)
}

Sources