Jason Mavandi
@mvndaai
The reason I know so much about errors
I have worked in Go for around five years.
Almost two years ago, my job asked me to design a way to have all APIs return a common JSON errors response.
I iterated over time on a helper package to make that common response easier. I have spent at least 100 hours on go errors and thought it would be nice to share.
Here is a public version of my current error implementation for creating a common response:
We are going to learn about errors by creating a package called caterr that will help compare errors to match categories. This will use the new Is, As, and Unwrap functions from Go1.13.
Here is my repo as a reference:
The try/catch of go
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r)
}
}()
panic("panicking")
}
// Recovered: panickingPanicking breaks the flow of an application. Go has a better way of handling errors that does not need to break the flow.
I agree with Mat Ryer who says the only time you should panic is to make another panic more readable.
Every application should have a "Pokémon" recover function to "catch 'em all". This prevents imported packages and unexpected panics in your code from completely crashing your application.
The power of interfaces
type error interface {
Error() string
}An error in go is just an interface
Any struct that includes a function Error() string fulfills the interface. Go allows multi-value returns. Although it is not required, the standard is if an error is returned, it is the last return named err when defined.
func foo() (int, error) {bar, err := foo()
if err != nil {
// handle the error
}func isZero(i int) error {
if i != 0 {
return errors.New("was not zero")
}
return nil
}
func isZero(i int) error {
if i != 0 {
return fmt.Errorf("it was '%s' not zero", i)
}
return nil
}package caterr
type Interface interface{
error
Category() interface{}
}
type impl struct {
message string
category interface{} //this will make sense later
}
func New(category interface{}, message string) error {
return &impl{ //This needs to be a pointer to allow nil
message: message,
category: category,
}
}
func (e *impl) Error() string {
return e.message
}
func (e *impl) Category() interface{} { return e.category }
Create a file called caterr.go and copy this:
Return, decorate, and handle
func foo() (int, error) {
a, err := bar()
if err != nil {
return 0, err
}
return a, nil
}A zero value of an interface in go is nil. If a function returns an error, check if it exists by if err != nil. If it does exist, pass it up.
Exiting the function at the first sight of an error makes go code safer than most languages.
func foo() error {
if err != nil {
return fmt.Errorf("in foo(): %s", err.Error())
}The package "github.com/pkg/errors" popularized the use of a Wrap function.
func foo() error {
if err != nil {
return errors.Wrap(err, "in foo()")
}Decorate an error to add more context. Skip capitalization and punctuation since you do not know how many times it will be decorated.
func foo() error {
...
if err != nil {
return fmt.Errorf("decoration: %w", err)
}
...
}Go 1.13 allows "%w" in fmt.Errorf to wrap an error.
Caution: This is a non-backward compatible change. If you use this Go 1.13 or higher is required.
func main(){
if err := foo(); err != nil{
log.Println("Caught an error:", err)
}
}When you make it to the top of a tree and cannot pass it up, handle the error. Only handle an error once. Common places for handling are main, http handlers, defer, and go functions.
func background() {
go func() {
if err := foo(); err != nil {
fmt.Println("Error from go func:", err)
}
}()
}// Update the struct
type impl struct {
message string
category interface{}
wrapped error // this is new
}Allows our error to wrap other errors.
// Add this function
func Wrap(err error, category interface{}, message string) error {
return &impl{
message: message,
category: category,
wrapped: err,
}
}Type checking?
if err != nil {
switch v := err.(type) {
case NotFound:
// ...
}
return err
}People want the ability to find out what kind of error is being handled.
This has become more complicated with the popularity of decorating errors. Using fmt.Error without "%w" removes the type, and Wrapping covers other types in a chain.
error chain: error -> NotFound -> DBNotFound -> error
A proposal was made to standardize on the ideas from the "github/pkg/errors" package to allow all errors to be decorated by wrapping.
An implementation that works on versions previous to Go 1.13 is xerrors.
https://godoc.org/golang.org/x/xerrors
Unwrap, As, Is
// interface { Unwrap() error }
func Unwrap(err error) error {
u, ok := err.(interface {
Unwrap() error
})
if !ok {
return nil
}
return u.Unwrap()
}There is a new function errors.Unwrap(). This returns an error that has been wrapped.
Unwrap expects errors to fulfill an interface of Unwrap() error.
// Update the interface
type Interface interface {
error
Category() interface{}
Unwrap() error // add this
}Allows our error to wrap other errors.
// Add this function
func (e *impl) Unwrap() error {
return e.wrapped
}// interface{ As(interface{}) bool }
func As(err error, target interface{}) bool {
...
if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
return true
}
...
}errors.As also uses an interface. This takes an error and a pointer to a target type. It will unwrap the error until the error's As function returns true. If it does the pointer will point to that error in the chain.
You get to decide what a match means.
// Update the interface
type Interface interface {
error
Category() interface{}
Unwrap() error
As(interface{}) bool // add this
}Allows our error to use As.
// Add this function
func (e *impl) As(err interface{}) bool {
if c, ok := err.(Interface); ok { //note the uses of our interface
return c.Category() == e.Category()
}
return false
}// interface{ Is(error) bool
func Is(err, target error) bool {
...
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
...
}
errors.Is compares two errors and sees if they match.
The match is determined by the error fulfillment of an interface.
Caution this could be misused.
func (e *impl) Is(err error) bool {
return true // this will always match
}
if err != nil {
if errors.Is(err, NotFound) {
//...
}
return err
}Is finally solves the issue with determining if errors are of a type.
Note: A switch cannot be used because the function call is necessary.
// Update the interface
type Interface interface {
error
Category() interface{}
Unwrap() error
As(interface{}) bool
Is(error) bool // add this
}Allows our error to use Is.
// Add this function
func (e *impl) Is(err error) bool {
return e.As(err)
}Did it work?
import (
"errors"
// errors "golang.org/x/xerrors" // if your go version < 1.13
)
func HasCategory(err error, category interface{}) bool {
return errors.Is(err, &impl{category: category})
}Helper function wrapping errors.Is
package caterr
import (
"errors"
"fmt"
"testing"
)
func TestWrap(t *testing.T) {
type s string
categories := []interface{}{1, "category", s("category")}
for _, category := range categories {
err := errors.New("errors")
if HasCategory(err, category) {
t.Error("this should not have matched the category")
}
err = Wrap(err, category, "caterr")
if !HasCategory(err, category) {
t.Error("did not match the category")
}
}
}Create a file called caterr_test.go and copy this:
And other tips
func main(){
foo() // this hides that an error is possible
_ := foo()
baz, _ := bar()
baz.String() // this will panic because of a nil pointer
}
type qux struct{
str string
}
func (q qux) String() { return q.str}
func foo() error {
return errors.New("")
}
func bar() (*qux, error) {
return nil, foo()
}
Errors, like all return variables, can be ignored by using "_". Never do this.
func bar(){ //avoid these
defer foo()
go foo()
}
func bar(){
defer func(){
if err := foo(); err != nil {
log.Println(err)
}
}()
go func(){
if err := foo(); err != nil {
log.Println(err)
}
}()
}
You cannot pass up an error in go/defer functions. They are often ignored, especially in packages. Try to avoid this. You can use anonymous functions to handle them.
func main(){
if err := foo(); err != nil {
handleError(err)
}
}
func foo() error {
defer func(){
if err := bar(); err != nil {
handleError(err)
}
}()
return errors.New("")
}
func handleError(err error) {
log.Println("[!] " + err.Error()) // decide how you want logs to look
// maybe increment a metric
// do something else
}
I want a standard configurable function, errors.Handle(error). Imported packages shouldn't have to choose how you handle errors in go/defer functions. Currently, they are often ignored or just logged. If you agree, give my proposal a thumbs up.
Don't just log. Decide how errors should be handled.
func main(){
err := worstExample
// these will cause a compile issue because "err" is of another type
err := badExample()
err = goodExample()
}
func worstExample() MyError { //cannot be nil
return MyError{}
}
func badExample() *MyError {
return &MyError{}
}
func goodExample() error {
return &MyError{}
}Even if you use another type, function returns should always be of the type error.
err := foo()
if err != nil {
...
}
if err := foo(); err != nil {
...
}
if _, err := bar(); err != nil {
...
}
baz, err := bar()
if err != nil {
...
}To avoid "err" escaping the scope of when it is handled use an inline if.
If you consider this a language bug like I do give my proposal a thumbs up.
func main() {
if err := foo(); err != nil {
log.Println(err)
}
}
func foo() error {
...
if err := bar(); err != nil {
log.Println(err) // bad
return err
}
...
}
func bar() error {
...
if err := foo(); err != nil {
log.Println(err) // bad
return err
}
...
}Only handle an error once.
This error will be mistakenly logged 3 times.
func foo() error {
if err := bar(); err != nil {
return err
}
if err != baz(); err != nil {
return err
}
return nil
}
func otherFoo() error {
if err := bar(); err == nil { // "==" instead of "!="" can be easily missed
if err != baz(); err != nil {
return err
}
return nil // hidden happy path
} else {
return err
}
return nil // unreachable code
}
Returning from functions as soon as you find an error avoids skipping errors. Not using "else" in Go lets the left side of your function be a happy path line of sight.
Comments