Skip to content

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

@atc0005

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

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