Jason Mavandi
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
}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)
}
}()
}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
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.
// 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.
// 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.
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 foo() error {
err := bar()
if err != nil {
return fmt.Errorf("decoration: %w")
}
}When using fmt.Errorf always use %w instead of %v so errors can be unwrapped and used in Is or As. Using %v will call .String() on the error.
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.
The package I created to give more context to errors
What
Why
func foo(req request) {
ctx := req.Context()
ctx = ctxerr.SetField(ctx, "params", req.Params)
if err := baz(ctx); err != nil {
err = ctxerr.QuickWrap(ctx, err)
ctxerr.Handle(err)
return
}
}
func bar(ctx context.Context) error {
// If err == nil; (Quick)Wrap will return nil
return ctxerr.QuickWrap(ctx, baz(ctx))
}
func baz(ctx context.Context) error {
return ctxerr.New(ctx, "NOT_IMPLEMENTED", "function not implemented")
}func foo() {
ctx := context.Background()
go func(ctx context.Context) {
err := bar(ctx)
if err != nil {
err = ctxerr.QuickWrap(ctx, err)
ctxerr.Handle(err)
}
}(ctx)
}
func bar(ctx context.Context) error {
return ctxerr.New(ctx, "code", "message")
}Handle is used to consistently handle errors. It should be used everywhere you cannot return an error (i.e main, HTTP requests, go funcs, and defer funcs)
Hooks can be added to make Handle do what you want like call your specific logger anything else
func foo(ctx context.Context) error {
if err != nil {
return ctxerr.NewHTTPf(ctx, "code", "action", http.StatusBadRequest, "%s", "baz")
}
}The package has a few fields that can autofill, like code. HTTP helper functions exist with special fields.
ctxerr.NewHTTP(ctx, "<code>", "<action>", http.StatusBadRequest, "<message>")
ctxerr.NewHTTPf(ctx, "<code>", "<action>", http.StatusConflict, "%s", "<vars>")
ctxerr.WrapHTTP(ctx, err, "<code>", "<action>", http.StatusBadRequest, "<message>")
ctxerr.WrapHTTPf(ctx, err, "<code>", "<action>", http.StatusBadRequest, "%s", "<vars>")Comments