Golang custom error types with stack trace
28 April 2019
Golang error handling is fun because it is a first class citizen in the language, but the built in behavior is bare bones. But there are two features that really help:
- Custom error types
- Stack traces
The default behavior returns an error with a message. An example is doing a sql
query where we bubble up ErrNoRows
and return 404 for that error but 500 for other errors:
// handler.go
import "database/sql"
func handler(r http.Reader, w *http.Writer) error {
user, err := repo.findUserByID(1)
if err != nil {
if err == sql.ErrNoRows {
return 404
} else {
return 500
}
}
return 200, user
}
// repo.go
import "database/sql"
func findUserByID(id int) (User, err) {
// ...
rows, err := sql.Query(...)
if err != nil && err == sql.ErrNoRows {
return User{}, err
}
// ...
}
These types of errors are called sentinel errors, the benefit is we can check against
their type, but the downside is our http handler now needs to import the database/sql
package
and we have no stack traces. We could define our own custom error type and return
that instead:
// custom.go
var ErrorNotFound = errors.New("Record not found")
func findUserByID(id int) (User, err) {
if err != nil && err == sql.ErrNoRows {
return User{}, ErrorNotFound
}
}
Under the hood the sql error looks like this:
var ErrNoRows = errors.New("sql: no rows in result set")
Declaring our own error does not get us too much farther, and we still have no stack trace. Our example has 1 layer and 1 query, but if we had multiple layers and multiple queries, keeping track of what went wrong where becomes less obvious (especially for errors less straight forward as ErrNotFound).
This is where a package like errors
comes in. We can use errors.Wrap(err, "msg")
and later up the stack get the original error with errors.Cause(err)
. Our
error handling code now gets a stack trace and has no need to import sql:
package main
import (
"fmt"
"github.com/pkg/errors"
)
var ErrorNotFound = errors.New("Record not found")
func main() {
err := foo("SELECT id FROM users where name = 'bar'")
if errors.Cause(err) == ErrorNotFound {
fmt.Printf("Holla %v\n%+v\n", err.Error(), err)
} else {
fmt.Println("default error")
}
}
func foo(s string) error {
return errors.Wrap(ErrorNotFound, s)
}
The trick to the errors package is in calling runtime.Callers()
when a new
error is created. Which you could also do if you wanted to roll something custom.
A potential way to improve would be not requiring the handler to import ErrorNotFound
.
A way to get around that would be to type assert on behavior instead of
types. Where if our ErrorNotFound instead conformed to an interface, we could check
against that, and just import an interface instead of a struct:
package main
import (
//"errors"
"fmt"
)
type ErrorNotFound struct{ msg string }
func (e *ErrorNotFound) IsNotFound() bool { return true }
func (e *ErrorNotFound) Error() string { return e.msg }
type IErrorNotFound interface {
error
IsNotFound() bool
}
func makeError() error {
return &ErrorNotFound{"foo"}
}
func main() {
err := makeError()
// type check on interface and call interface method
if e, ok := err.(IErrorNotFound); ok && e.IsNotFound() {
fmt.Println("error not found: ", e)
}
}
Golang has recently implemented this style of approach in the net package.