Golang errors.Is() does not work for Custom error?

eye-catch Golang

It’s important to define a custom error to differentiate errors so that we can handle them differently depending on the error type. How can a custom error be implemented in Golang? Doesn’t comparison logic work as expected for a custom error? Let’s learn to solve the problems here.

Sponsored links

Predefined Error

An error can be defined with errors.New() but it’s not good to use it without assigning it to a variable. If we need an error, it should be predefined as a static value in the following way.

import "errors"

var ErrPredefinedError = errors.New("predefined error message")

Then, we can use this error everywhere. This is a kind of custom error with a static error message.

Sponsored links

Custom Error struct

If some functions are needed for a custom error, we need to define it with a struct data type. Let’s check how it’s implemented.

Without property

If we want to create our own error, we need to define a struct that implements Error() method.

type ErrorWithoutPrep struct{}

func (e *ErrorWithoutPrep) Error() string {
    return "error without prep"
}

func (e *ErrorWithoutPrep) ErrorCode() int {
    return 11
}

It might be a rare case but we can define another method too like ErrorCode above.

err := &ErrorWithoutPrep{}
err.ErrorCode()

If the custom method is not needed, it’s the same as a predefined error above. We don’t have to define a struct in this case.

With property

It doesn’t make much sense to create a custom error that doesn’t have a property. We can dynamically generate the error message by using the defined properties. That’s one of the good reasons to define a custom error.

type ErrorWithPrep struct {
    Name string
}

func (e *ErrorWithPrep) Error() string {
    return fmt.Sprintf("error with prep, name: %s", e.Name)
}

When the error is converted to string, Error() method is called and thus the error message is generated with the specified name in this example. This can’t be achieved with a predefined error.

How to compare two errors

In Golang, an error is propagated to the caller. If a called function A calls a function B, the error returned by function B is wrapped by function A. The error is something like errorA(errorB). How can we check the nested error type if we need to change the logic depending on the error?

We can use errors.As() or errors.Is().

errors.As() is used to check the error type while errors.Is() is used to check the properties.

errors.As: How to compare the two error types

Let’s learn about errors.As() first.

  • It compares the data type
  • It does NOT compare the properties
  • It can be used for a wrapped error too

errors.As requires an error at the first parameter and the address of the pointer of the target error type. at the second parameter. It sounds complicated but it’s easy to understand it when you see the code.

var withoutPrep *ErrorWithoutPrep
wrapped = fmt.Errorf("wrapped error: %w", &ErrorWithoutPrep{})
// second parameter is address of the pointer of the error type
result1 := errors.As(&ErrorWithoutPrep{}, &withoutPrep)
result2 := errors.As(wrapped, &withoutPrep)
fmt.Printf("%v\t%v\n", result1, result2) // true        true

The comparison with a wrapped error also returns true. It should be a different error type but errors.As function unwraps the error internally. Hence, the original error is revealed to be compared.

It returns true even if the error has properties. It doesn’t check the value as you can see in the code.

var withPrep *ErrorWithPrep
wrapped = fmt.Errorf("wrapped error: %w", &ErrorWithPrep{Name: "Yuto"})
result1 = errors.As(&ErrorWithPrep{Name: "Yuto"}, &withPrep)
result2 = errors.As(wrapped, &withPrep)
fmt.Printf("%v\t%v\n", result1, result2) // true        true

fmt.Println(errors.As(&ErrorWithoutPrep{}, &withPrep)) // false

Of course, it returns false if the error type is different.

errors.Is: How to compare the two errors

How can we implement if we need to check the properties? Use errors.Is() in this case.

I defined the following function to make it easier to write the code.

func runIs(original error, expected error) {
    result1 := errors.Is(original, expected)
    wrapped := fmt.Errorf("wrapped error: %w", original)
    result2 := errors.Is(wrapped, expected)
    fmt.Printf("%v\t %v\n", result1, result2)
}

Let’s check the result here.

runIs(ErrPredefinedError, ErrPredefinedError)   // true     true
runIs(&ErrorWithoutPrep{}, &ErrorWithoutPrep{}) // true     true
runIs(&ErrorWithPrep{}, &ErrorWithPrep{})       // false    false
runIs(&ErrorWithPrep{Name: "Yuto"}, &ErrorWithPrep{Name: "Yuto"}) // false    false

If the error doesn’t have any property, errors.Is() returns true. However, it returns false for an error with properties. Why? Let’s see the implementation of errors.Is().

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
        }
        switch x := err.(type) {
        case interface{ Unwrap() error }:
            err = x.Unwrap()
            if err == nil {
                return false
            }
        case interface{ Unwrap() []error }:
            for _, err := range x.Unwrap() {
                if Is(err, target) {
                    return true
                }
            }
            return false
        default:
            return false
        }
    }
}

To make it return true, the error must be comparable or Is() must be implemented. If it returns false, it unwraps the error and repeats the process. Our custom error type defined above doesn’t implement Is(). Therefore, the result was false in this example.

Implement Is() for a custom error

We found that Is() needs to be implemented for a custom error. Let’s implement it.

type ErrorIsImpl struct {
    Name string
    ID   int
}

func (e *ErrorIsImpl) Error() string {
    return fmt.Sprintf("error is impl, name: %s", e.Name)
}

func (e *ErrorIsImpl) Is(err error) bool {
    other, ok := err.(*ErrorIsImpl)
    if !ok {
        return false
    }

    return other.Name == e.Name && other.ID == e.ID
}

There are two properties this time. The first 4 lines in Is() checks whether the error is the same type or not. If not, it returns false of course. If it’s the same type, it compares the desired properties.

By defining Is(), errors.Is() returns true only if the two properties are the same.

runIs(&ErrorIsImpl{Name: "Yuto"}, &ErrorIsImpl{Name: "Yuto", ID: 2})        // false     false
runIs(&ErrorIsImpl{Name: "Yuto", ID: 1}, &ErrorIsImpl{Name: "Yuto", ID: 1}) // true     true
runIs(&ErrorIsImpl{Name: "Yuto"}, &ErrorIsImpl{Name: "NN"})                 // false    false

Since errors.As() checks only the type, it’s not necessary to implement.

var isImpl *ErrorIsImpl
wrapped = fmt.Errorf("wrapped error: %w", &ErrorIsImpl{Name: "Yuto"})
result1 = errors.As(&ErrorIsImpl{Name: "Yuto"}, &isImpl)
result2 = errors.As(wrapped, &isImpl)
fmt.Printf("%v\t%v\n", result1, result2) // true        true

fmt.Println(errors.As(&ErrorIsImpl{Name: "Yuto"}, &withPrep)) // false

Unwrap an error in case a custom error wraps an error

If a custom error wraps an error and the wrapped error needs to be compared, we need to implement Unwrap().

type WrappedError struct {
    OriginalError error
}

func (e *WrappedError) Error() string {
    return fmt.Sprintf("wrapped error, original: %s", e.OriginalError.Error())
}

func (e *WrappedError) Unwrap() error {
    return e.OriginalError
}

As we saw in the implementation of errors.Is(), Unwrap() is called repeatedly. If a custom error doesn’t implement it, the comparison ends and false is returned. To make it work, we need Unwrap() implementation for a custom error. If there are multiple errors, it can return []error instead.

var withPrep *ErrorWithPrep
original := ErrorWithPrep{Name: "Yuto"}
wrappedError := &WrappedError{OriginalError: &original}
fmt.Println(errors.As(wrappedError, &withPrep)) // true
fmt.Println(errors.Is(wrappedError, withPrep))  // true

Difference between errors.Is() and errors.As()

errors.Is() compares the property values. A custom error needs to implement Is() to make it work.

errors.As() compare the error type but not property values.

Comments

Copied title and URL