More than you ever wanted to know about Go errors

Jason Mavandi

panic, defer, recover

The try/catch of go

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

// Recovered: panicking

Example

Don't Panic

When is it OK to panic?

Panicking 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.

When should I recover?

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.

error

The power of interfaces

type error interface {
    Error() string
}

What is an error?

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
}

Creating an error

The normal way of creating an error is by using the errors or fmt packages.

What do you do with errors?

Return, decorate, and handle

func foo() (int, error) {
    a, err := bar()
    if err != nil {
        return 0, err
    }
    return a, nil
}

Normal Flow - Pass it up

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())
    }

Decoration

The package "github.com/pkg/errors" popularized the use of a Wrap function.

2
 Advanced issues found
2
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)
    }
    ...
}

%w

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)
    }
}

Handling

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)
        }
    }()
}

Why errors changed in Go1.13?

Type checking?

if err != nil {
    switch v := err.(type) {
    case NotFound:
        // ...
    }
    return err
}

Type Handling

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

Additions to errors

Unwrap, As, Is

// interface { Unwrap() error }

func Unwrap(err error) error {
	u, ok := err.(interface {
		Unwrap() error
	})
	if !ok {
		return nil
	}
	return u.Unwrap()
}

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
    }
    ...
}

As

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.

1
// interface{ Is(error) bool 

func Is(err, target error) bool {
    ...
        if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
            return true
        }
    ...
}

Is

 

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.

1
func (e *impl) Is(err error) bool {
    return true // this will always match
}
if err != nil {
    if errors.Is(err, NotFound) {
        //...
    }
    return err
}

Solving the type issue

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.

General Guidelines

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()
}

Never squash an error

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)
        }
    }()
}	

Not even in go/defer funcs

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")
    }
}

Always use %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
}

Decide how to handle

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{}
}

Only return "error"

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 {
   ...
}

Scoping errors

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
    }
    ...
}

Return or Handle

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
}

Return quickly/avoid "else"

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.

ctxerr

The package I created to give more context to errors

https://github.com/mvndaai/ctxerr

What and Why

What

  • ctxerr stores a map[string]interface{} (fields) onto a go context.Context (ctx)
  • It saves that Context in the error that is returned.

 

Why

  • Most Go web applications already pass a ctx from the request to  when requests are canceled
  • Fields can be at the topmost function so all errors below include the context (i.e. IDs, ....)
  • Logs are easier to parse when some context is added as JSON fields rather than part of the message

 

 
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")
}

Quick Usage Overview

  • Error codes are the only thing you need to decide on
 
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 function

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")
    }
}

HTTP usage

The package has a few fields that can autofill, like code. HTTP helper functions exist with special fields.

  • statusCode - HTTP status code
  • action - Message that can be return to an error to show what action they can take to fix the error and hide our internal implementation
 
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>")

Q&A

Comments

Go Errors - Updated

By Jason Mavandi

Go Errors - Updated

More than you every wanted to know about errors

  • 391