DEV Community

Cover image for [Go] new feature for wrapped errors in 1.13
Julian-Chu
Julian-Chu

Posted on

[Go] new feature for wrapped errors in 1.13

#go

Go 1.13 introduced new feature for wrapped errors. If you didn't heard about it, I strongly recommend you to read the article form Go Blog.
Working with Errors in Go 1.13

Let's write some code to understand new feature.

errors.Is

Go errors are values, so we use == to compare an error to specific value.
Since Go 1.13, We could use errors.Is(..) to replace "==" when comparing errors.

// simple error example
var TimeoutErr = errors.New("timeout")

func main() {
    err := TimeoutErr
    fmt.Printf("err Type: %T\n", err)
    // before Go 1.13
    fmt.Println("Is Timeout error? " + strconv.FormatBool(err == TimeoutErr))
    //  Go 1.13
    fmt.Println("Is Timeout error? " + strconv.FormatBool(errors.Is(err, TimeoutErr)))
}

------output-----
err Type: *errors.errorString
Is Timeout error? true
Is Timeout error? true

It is more understandable and reveal good intention for comparing errors. And it's not only that. errors.Is is more powerful for wrapped errors. Let's add wrapped error to the previous code

// wrapped error example
var TimeoutErr = errors.New("timeout")

func main() {
    err := getWrapperError()
    fmt.Printf("err Type: %T\n", err)
    // before Go 1.13
    fmt.Println("Is Timeout error using '==' ? " + strconv.FormatBool(err == TimeoutErr))
    //  Go 1.13
    fmt.Println("Is Timeout error using error.Is ?" + strconv.FormatBool(errors.Is(err, TimeoutErr)))
}

func getWrappedError() error {
    err:= fmt.Errorf("1st wrapped error: %w", TimeoutErr)
    return fmt.Errorf("2nd wrapped error: %w", err)
}
-----output-----
err Type: *fmt.wrapError
Is Timeout error using '==' ? false
Is Timeout error using error.Is ?true

Timeout error is wrapped by 2 fmt.Errorf(). The typical '==' comparison can not figure out it, but errors.Is can.

We could test it with custom error struct as well

// wrapped error example with custom struct
var TimeoutErr = errors.New("timeout")

func main() {
    err := &WrappedError{Err:TimeoutErr}
    fmt.Printf("err Type: %T\n", err)
    // before Go 1.13
    fmt.Println("Is Timeout error using '==' ? " + strconv.FormatBool(err == TimeoutErr))
    //  Go 1.13
    fmt.Println("Is Timeout error using error.Is ?" + strconv.FormatBool(errors.Is(err, TimeoutErr)))
}


type WrappedError struct {
    Err error
}

func (w WrappedError) Error() string {
    return "wrapped error: " + w.Err.Error()
}

func (w WrappedError) Unwrap() error {
    return w.Err
}

-----output-----
err Type: *main.WrappedError
Is Timeout error using '==' ? false
Is Timeout error using error.Is ?true

Ok, it works well,too.

errors.Is provides us the capability to detect the wrapped error.

Unwrap, and fmt.Errorf with '%w'

In previous examples, you could see two new things:

  • Unwrap() error{}
  • fmt.Errorf("....%w", err)

Let's understand them.

Unwrap method

Unwrap method is the key to access the wrapped error. For errors have Unwrap method, errors.Is can call the Unwrap to get previous error and do comparison. Here is the custom error struct from previous example.
You can remove the Unwrap method and run example again, then errors.Is can't work on the wrapped error.

type WrappedError struct {
    Err error
}

func (w WrappedError) Error() string {
    return "wrapped error: " + w.Err.Error()
}

// errors.Is call this method to get Err
func (w WrappedError) Unwrap() error {
    return w.Err
}

After understanding Unwrap method, it's easy to understand errors.Is under the hood.

source code

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


func Is(err, target error) bool {
    if target == nil {
        return err == target
    }

    isComparable := reflectlite.TypeOf(target).Comparable()
    for {
        if isComparable && err == target {
            return true
        }
        if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
            return true
        }
        // TODO: consider supporting target.Is(err). This would allow
        // user-definable predicates, but also may allow for coping with sloppy
        // APIs, thereby making it easier to get away with them.
        if err = Unwrap(err); err == nil {
            return false
        }
    }
}

If current error is not the target, errors.Is will use Unwrap to get next error until no next error or it's the target.

fmt.Errorf + '%w'

We know fmt.Errorf, but what's %w? In a nutshell, '%w' make fmt.Errorf return a error which implemented Unwrap method. You can change the '%w' to '%v' in previous example, and run the code again. The errors.Is will not work on wrapped error.

source code

func Errorf(format string, a ...interface{}) error {
    p := newPrinter()
    p.wrapErrs = true
    p.doPrintf(format, a)
    s := string(p.buf)
    var err error
    if p.wrappedErr == nil {
        err = errors.New(s)
    } else {
          // %w makes here work
        err = &wrapError{s, p.wrappedErr}
    }
    p.free()
    return err
}

type wrapError struct {
    msg string
    err error
}

func (e *wrapError) Error() string {
    return e.msg
}

func (e *wrapError) Unwrap() error {
    return e.err
}

errors.As

Last new feature is errors.As. It's used to test and cast current error to custom error struct. Behind the scenes, it uses Unwrap method as well. After successful casting, you could access field of wrapped error.

var TimeoutErr = errors.New("timeout")

func main() {
    err := getErr()
    fmt.Printf("err Type: %T\n", err)
    /// before Go 1.13
    // if
    if err, ok := err.(*WrappedError); ok {
        if _, ok := err.Err.(*DataBaseError); ok {
            fmt.Println("Is Database error using type assertion ? true")
        }
    }
    // switch
    switch err.(type) {
    case *WrappedError:
        err, _ := err.(*WrappedError)
        switch err.Err.(type) {
        case *DataBaseError:
            fmt.Println("Is Database error using type assertion ? true")
        }
    }
    ///  Go 1.13
    var dbErr2 *DataBaseError
    //if
    fmt.Println("Is Database error using error.As ? " + strconv.FormatBool(errors.As(err, &dbErr2)))
    // switch
    switch {
    case errors.As(err, &dbErr2):
        fmt.Println("Is Database error using error.As ? " + strconv.FormatBool(errors.As(err, &dbErr2)))
    }
    fmt.Println("Database error in "+ dbErr2.DbName)
}

func getErr() error {
    dbErr := &DataBaseError{Err: TimeoutErr, DbName:"Test Database"}
    err := &WrappedError{Err: dbErr}
    return err
}

type DataBaseError struct {
    DbName string
    Err    error
}

func (d DataBaseError) Error() string {
    return "database error in " + d.DbName + " err:" + d.Err.Error() + " \n"
}

func (d DataBaseError) Unwrap() error {
    return d.Err
}

type WrappedError struct {
    Err error
}

func (w WrappedError) Error() string {
    return "wrapped error: " + w.Err.Error()
}

func (w WrappedError) Unwrap() error {
    return w.Err
}

-----output-----
err Type: *main.WrappedError
Is Database error using type assertion ? true
Is Database error using type assertion ? true
Is Database error using error.As ? true
Is Database error using error.As ? true
Database error in Test Database


Comparison between errors.Is and errors.As

  • errors.As can cast error to specific error struct.
  • errors.Is is to compare error with error instance(like errors made by errors.New()), errors.As is to compare with specific error type. please see the following example.

var TimeoutErr = errors.New("timeout")

func main() {
    err := getErr()
    fmt.Printf("err Type: %T\n", err)
    fmt.Println("Is Timeout error using error.As ? " + strconv.FormatBool(errors.Is(err, TimeoutErr )))
    var dbErr *DataBaseError
    fmt.Println("Is Database error using error.As ? " + strconv.FormatBool(errors.As(err, &dbErr)))
}


----- output -----
err Type: *main.WrappedError
Is Timeout error using error.As ? true
Is Database error using error.As ? true

In the source code, you can find the type of target in the method signature

source code

func Is(err, target error) bool {
........
}


func As(err error, target interface{}) bool {
.........
}

Conclusion

The new feature introduced in Go 1.13 makes error handling more elegant. If you have used try-catch in other programming language, you can think it works like catching specific exception in the high level method.

Top comments (0)