Skip to content

errwrap (incorrectly?) identifies missing error-wrapping directive %w #21

Description

@atc0005

Overview

Hi @fatih,

Thank you for providing this linter. It has already helped catch several cases where I did not use the %w error-wrapping directive in my projects.

It did however flag one case that may be a false-positive.

I have a project that is still using Go 1.19 and the linter identifies what it believes to be a missing use of %w, but I already have one such usage in place. I understand that this syntax would be valid in Go 1.20, but since the project README indicates that the linter supports Go 1.16 and newer I'm reporting this as a potential problem.

Short version (before following linter advice):

annotatedErrors := make([]error, 0, len(errs))

// ...
annotatedErrors = append(annotatedErrors, fmt.Errorf(
    "%w: %v; %s",
    err,
    errRuntimeTimeoutReached,
    runtimeTimeoutReachedAdvice,
))

Linter output:

$ errwrap main.go
main.go:41:48: call could wrap the error with error-wrapping directive %w

Reproducible example

Code after following linter advice:

code example
// main.go
package main

import (
	"context"
	"errors"
	"fmt"
)

// errRuntimeTimeoutReached indicates that plugin runtime exceeded specified
// timeout value.
var errRuntimeTimeoutReached = errors.New("plugin runtime exceeded specified timeout value")

// runtimeTimeoutReachedAdvice offers advice to the sysadmin for routine
// occurrence.
const runtimeTimeoutReachedAdvice string = "consider increasing value if this is routinely encountered"

func annotateError(errs ...error) []error {

	isNilErrCollection := func(collection []error) bool {
		if len(collection) != 0 {
			for _, err := range errs {
				if err != nil {
					return false
				}
			}
		}
		return true
	}

	switch {

	// Process errors as long as the collection is not empty or not composed
	// entirely of nil values.
	case !isNilErrCollection(errs):
		annotatedErrors := make([]error, 0, len(errs))

		for _, err := range errs {
			if err != nil {
				switch {
				case errors.Is(err, context.DeadlineExceeded):
					annotatedErrors = append(annotatedErrors, fmt.Errorf(
						"%w: %w; %s",
						err,
						errRuntimeTimeoutReached,
						runtimeTimeoutReachedAdvice,
					))

				default:
					// Record error unmodified if additional decoration isn't defined
					// for the error type.
					annotatedErrors = append(annotatedErrors, err)
				}
			}
		}

		return annotatedErrors

	// No errors were provided for evaluation.
	default:
		return nil
	}

}

func main() {
	sampleErr := fmt.Errorf(
		"my sample error: %w",
		context.DeadlineExceeded,
	)

	errCollection := annotateError(sampleErr)

	fmt.Println(errCollection)
}

// main_test.go
package main

import (
	"context"
	"fmt"
	"testing"
)

func TestAnnotateError(t *testing.T) {
	sampleErr := fmt.Errorf(
		"my sample error: %w",
		context.DeadlineExceeded,
	)

	errCollection := annotateError(sampleErr)

	t.Log(errCollection)
}

Results

If I follow the linter's advice, this is what I get when running the code:

$ go run main.go
[my sample error: context deadline exceeded: %!w(*errors.errorString=&{plugin runtime exceeded specified timeout value}); consider increasing value if this is routinely encountered]

$ go test ./...
# errwrap-false-positive
.\main.go:41:48: fmt.Errorf call has more than one error-wrapping directive %w
FAIL    errwrap-false-positive [build failed]
FAIL

Linter version details

$ go version -m $(which errwrap.exe)
C:/Users/atc0005/go/bin/errwrap.exe: go1.19.7
        path    github.com/fatih/errwrap
        mod     github.com/fatih/errwrap        v1.5.0  h1:/z6jzrekbYYeJukzq9h3nY+SHREDevEB0vJYC4kE9D0=
        dep     golang.org/x/mod        v0.8.0  h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
        dep     golang.org/x/sys        v0.5.0  h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
        dep     golang.org/x/tools      v0.6.0  h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
        build   -compiler=gc
        build   CGO_ENABLED=1
        build   CGO_CFLAGS=
        build   CGO_CPPFLAGS=
        build   CGO_CXXFLAGS=
        build   CGO_LDFLAGS=
        build   GOARCH=amd64
        build   GOOS=windows
        build   GOAMD64=v1

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions