TODO Error Handling

catch panics

Simple example how catch a panic:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import (
	"fmt"
)

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recover", r)
        }
    }()

    f()
}

func f() {
    fmt.Println("a")
    panic("foo")
    fmt.Println("b")
}

wrap errors

Using the %w directive:

1
2
3
if err != nil {
    return fmt.Errorf("bar failed: %w", err)
}

Using the %v directive:

1
2
3
if err != nil {
    return fmt.Errorf("bar failed: %v", err)
}
Code Snippet 1: In this case the error is not wraped. It's transformed into another error, to add context. The original source error is no longer available.

check is an error is of a certain type (errors.As)

In summary, if we rely on Go 1.13 error wrapping, we must use errors.As to check whether an error is a specific type. This way, regardless of whether the error is returned directly by the function we call or wrapped inside an error, errors.As will be able to recursively unwrap our main error and see if one of the errors is a specific type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func handler(w http.ResponseWriter, r *http.Request) {
    // Get transaction ID

    amount, err := getTransactionAmount(transactionID)
    if err != nil {
        if errors.As(err, &transientError{}) {
            http.Error(w, err.Error(),
                http.StatusServiceUnavailable)
        } else {
            http.Error(w, err.Error(),
                http.StatusBadRequest)
        }
        return
    }

    // Write response
}

errors.Is

In summary, if we use error wrapping in our application with the %w directive and fmt.Errorf, checking an error against a specific value should be done using errors.Is instead of ==. Thus, even if the sentinel error is wrapped, errors.Is can recursively unwrap it and compare each error in the chain against the provided value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import "errors"

var ErrFoo = errors.New("foo")

err := query()
if err != nil {
    if errors.Is(err, sql.ErrNoRows) {
        // ...
    } else {
        // ...
    }
}

handling defer errors

Some example (in this example the error in the defer function is completely ignored):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func getBalance(db *sql.DB, clientID string) (
    balance float32, err error) {
    rows, err := db.Query(query, clientID)
    if err != nil {
        return 0, err
    }
    defer func() {
        err = rows.Close()
    }()

    if rows.Next() {
        err := rows.Scan(&balance)
        if err != nil {
            return 0, err
        }
        return balance, nil
    }
    // ...
}

Final version:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
defer func() {
    closeErr := rows.Close()
    if err != nil {
        if closeErr != nil {
            log.Printf("failed to close rows: %v", err)
        }
        return
    }
    err = closeErr
}()