A Go linter to check that errors from external packages are wrapped

Wrapcheck

Go Report Card

A simple Go linter to check that errors from external packages are wrapped during return to help identify the error source during debugging.

More detail in this article

Install

Go >= v1.16

$ go install github.com/tomarrell/wrapcheck/v2/cmd/wrapcheck@v2

Wrapcheck is also available as part of the golangci-lint meta linter. Docs and usage instructions are available here. When used with golangci-lint, configuration is integrated with the .golangci.yaml file.

Configuration

You can configure wrapcheck by using a .wrapcheck.yaml file in either the local directory, or in your home directory.

# An array of strings which specify substrings of signatures to ignore. If this
# set, it will override the default set of ignored signatures. You can find the
# default set at the top of ./wrapcheck/wrapcheck.go.
ignoreSigs:
- .Errorf(
- errors.New(
- errors.Unwrap(
- .Wrap(
- .Wrapf(
- .WithMessage(

Usage

To lint all the packages in a program:

$ wrapcheck ./...

Testing

This linter is tested using analysistest, you can view all the test cases under the testdata directory.

TLDR

If you've ever been debugging your Go program, and you've seen an error like this pop up in your logs.

time="2020-08-04T11:36:27+02:00" level=error error="sql: error no rows"

Then you know exactly how painful it can be to hunt down the cause when you have many methods which looks just like the following:

func (db *DB) getUserByID(userID string) (User, error) {
	sql := `SELECT * FROM user WHERE id = $1;`

	var u User
	if err := db.conn.Get(&u, sql, userID); err != nil {
		return User{}, err // wrapcheck error: error returned from external package is unwrapped
	}

	return u, nil
}

func (db *DB) getItemByID(itemID string) (Item, error) {
	sql := `SELECT * FROM item WHERE id = $1;`

	var i Item
	if err := db.conn.Get(&i, sql, itemID); err != nil {
		return Item{}, err // wrapcheck error: error returned from external package is unwrapped
	}

	return i, nil
}

The problem here is that multiple method calls into the sql package can return the same error. Therefore, it helps to establish a trace point at the point where error handing across package boundaries occurs.

To resolve this, simply wrap the error returned by the db.Conn.Get() call.

func (db *DB) getUserByID(userID string) (User, error) {
	sql := `SELECT * FROM user WHERE id = $1;`

	var u User
	if err := db.Conn.Get(&u, sql, userID); err != nil {
		return User{}, fmt.Errorf("failed to get user by ID: %v", err) // No error!
	}

	return u, nil
}

func (db *DB) getItemByID(itemID string) (Item, error) {
	sql := `SELECT * FROM item WHERE id = $1;`

	var i Item
	if err := db.Conn.Get(&i, sql, itemID); err != nil {
		return Item{}, fmt.Errorf("failed to get item by ID: %v", err) // No error!
	}

	return i, nil
}

Now, your logs will be more descriptive, and allow you to easily locate the source of your errors.

time="2020-08-04T11:36:27+02:00" level=error error="failed to get user by ID: sql: error no rows"

A further step would be to enforce adding stack traces to your errors instead using errors.WithStack() however, enforcing this is out of scope for this linter for now.

Why?

Errors in Go are simple values. They contain no more information about than the minimum to satisfy the interface:

type Error interface {
  Error() string
}

This is a fantastic feature, but can also be a limitation. Specifically when you are attempting to identify the source of an error in your program.

As of Go 1.13, error wrapping using fmt.Errorf(...) is the recommend way to compose errors in Go in order to add additional information.

Errors generated by your own code are usually predictable. However, when you have a few frequently used libraries (think sqlx for example), you may run into the dilemma of identifying exactly where in your program these errors are caused.

In other words, you want a call stack.

This is especially apparent if you are a diligent Gopher and always hand your errors back up the call stack, logging at the top level.

So how can we solve this?

Solution

Wrapping errors at the call site.

When we call into external libraries which may return an error, we can wrap the error to add additional information about the call site.

e.g.

...

func (db *DB) createUser(name, email, city string) error {
  sql := `INSERT INTO customer (name, email, city) VALUES ($1, $2, $3);`

  if _, err := tx.Exec(sql, name, email, city); err != nil {
    // %v verb preferred to prevent error becoming part of external API
    return fmt.Errorf("failed to insert user: %v", err)
  }

  return nil
}

...

This solution allows you to add context which will be handed to the caller, making identifying the source easier during debugging.

Contributing

As with most static analysis tools, this linter will likely miss some obscure cases. If you come across a case which you think should be covered and isn't, please file an issue including a minimum reproducible example of the case.

License

This project is licensed under the MIT license. See the LICENSE file for more details.

Owner
Tom Arrell
Senior Backend Engineer @sumup — Lover of Rust and Go, hack around building keyboards when I'm bored
Tom Arrell
Comments
  • Configuration to define what is external

    Configuration to define what is external

    Thanks for the nice linter!

    I was curious if you considered cases where the meaning of external could mean something other than outside the current package. In my case I would like to find cases where errors from outside the current module or -local string are not wrapped. I see the levels being package, module, organization. If this interests you I'd like to work on it with you otherwise I could simply fork the project

  • Proposal: ignore returning value of specify package's function

    Proposal: ignore returning value of specify package's function

    Hello 👋 Thank you for developing such a great tool!

    I'm using a library that wraps and manages errors like below.

    https://github.com/morikuni/failure https://github.com/pkg/errors

    Currently, wrapcheck also reports for these librarie's function.

    example

    error returned from external package is unwrapped (wrapcheck)
                    return nil, errors.Wrap(err, "failed to do something")
                                      ^
    

    This is, of course, the correct behavior. But I don't need these report from wrapcheck. So, I ignore these reports with golangci-lint's exclude-rules like below.

    issues:
      exclude-rules:
       - source: "errors"
          linters:
            - wrapcheck
        - source: "failure"
          linters:
            - wrapcheck
    

    https://golangci-lint.run/usage/configuration/

    It would be very helpful to be able to configure these settings on the wrapcheck side.

    (This proposal may be similar to #2)

    Thanks

  • Consider whitelisting cases where function has single error return case

    Consider whitelisting cases where function has single error return case

    With the following code:

    https://github.com/flexkube/libflexkube/blob/9e0182c5c9e97e748e5945d122d686aebd4b4e6e/cli/flexkube/resource.go#L352-L374

    I think the linter should allow to skip error wrapping, as the caller should do the wrapping. If error occurs, it is known that the error comes from this line. What do you think? Or maybe this should be somehow configurable?

  • ignorePackageGlobs doesn't appear to be working

    ignorePackageGlobs doesn't appear to be working

    Hey, great tool! This has been really useful to track down places I can apply some new error context packages I've been working on to provide additional structured information to errors.

    After applying error contexts across our project I noticed a few places where it was unnecessary so I decided to disable reporting of imports from local packages (/pkg, etc)

    However, maybe I'm misunderstanding this config option, but it doesn't appear to do what I expected.

    Here's the configuration file: https://github.com/Southclaws/storyden/blob/main/.golangci.yml#L28

    And here are the offending lines:

    • https://github.com/Southclaws/storyden/blob/main/pkg/transports/http/bindings/threads.go#L39
    • https://github.com/Southclaws/storyden/blob/main/pkg/transports/http/bindings/threads.go#L52

    I assumed that a ignorePackageGlobs value of github.com/Southclaws/storyden/* would ignore these two lines as they are functions that are imported from within this pattern:

    • i.thread_svc.Create: github.com/Southclaws/storyden/pkg/services/thread.Service.Create
    • i.thread_svc.ListAll: github.com/Southclaws/storyden/pkg/services/thread.Service.Create

    The actual output from running golangci-lint run:

    storyden on  main [!] via 🐹 v1.18.3 on ☁️  (eu-central-1) 
    ❯ golangci-lint run
    
    pkg/transports/http/bindings/threads.go:39:10: error returned from interface method should be wrapped: sig: func (github.com/Southclaws/storyden/pkg/services/thread.Service).Create(ctx context.Context, title string, body string, authorID github.com/Southclaws/storyden/pkg/resources/account.AccountID, categoryID github.com/Southclaws/storyden/pkg/resources/category.CategoryID, tags []string) (*github.com/Southclaws/storyden/pkg/resources/thread.Thread, error) (wrapcheck)
                    return err
                           ^
    pkg/transports/http/bindings/threads.go:52:10: error returned from interface method should be wrapped: sig: func (github.com/Southclaws/storyden/pkg/services/thread.Service).ListAll(ctx context.Context, before time.Time, max int) ([]*github.com/Southclaws/storyden/pkg/resources/thread.Thread, error) (wrapcheck)
                    return err
                           ^
    

    I wondered if it was golangci lint just using an outdated version but this occurs on the latest standalone version of wrapcheck too.

    I tried with a .wrapcheck.yaml file with:

    ignorePackageGlobs:
      - github.com/Southclaws/storyden/*
      - github.com/Southclaws/storyden/pkg/*
      - pkg/*
      - ./pkg/*
    

    but I can't seem to get any patterns to ignore correctly.

    Thanks!

  • dev: refactor `regexp` usage

    dev: refactor `regexp` usage

    This PR intends to bring the next features/fixes to wrapcheck:

    1. avoid compiling regexp rules into regexp.Regexp for checking each error returning function.
    2. removing os.Exit(1) in case fail to compile regexp rule from the codebase ( we can't handle such cases in golangci-lint: output is hidden, so no log messages received, which ends in no issues or errors comes from wrapcheck if bad regexp provided).

    P.S. The rest of the coding is changed by gofumpt automatically. P.P.S. It would be also great to release fix changes asap (so we update golangci-lint).

  • Great linter! Just one question about %w vs %v

    Great linter! Just one question about %w vs %v

    Hi,

    Forgive my question but I think there is a reason you are using %v rather than %w on errorf to wrap errors but I am not sure I understand it.

    I have always been using %w with returning to new errors that wrap previous ones but I notice here you are using %v.

    What is the reason ?

    Thanks again!

  • wrapcheck on goerr113 wrapping %w

    wrapcheck on goerr113 wrapping %w

    Wrapcheck on goerr113 wrapping %w

    I understand that Wrapcheck linter is all about wrapping the error coming from external packages but I think the error coming from the external package should never be used as %w reason being that this forces DevUsers to import the package A where the error is being returned as well as the internal package B where the error is being thrown and possible where the Sentinel lives if we ever need to check on the error type with Error.Is and Error.As creating a form of coupling to the internal detail implementation of package A

    import "pkg/Process"
    
    var ErrProccesFailed = "process failed due"
    {...}
    err := Process()
    return fmt.Errorf("%w : %v", ErrProccesFailed,err)
    

    I think Sentinel errors need to be created to wrap the err coming from the external package so only one import is needed to check on the Error.Is or Error.As meanwhile, we make sure the external package error is wrapped into a more meaningful error at this package level without exposing any type of possible coupling

  • New release?

    New release?

    I'd like to use the new ignorePackageGlobs feature

    https://github.com/tomarrell/wrapcheck/commit/d409df3395169cccdb7f39aea3092e20c5e69a9c

    But it wasn't released yet.

    Do you plan to release it?

  • Support for errors.Wrap(f)

    Support for errors.Wrap(f)

    Hello,

    In our project we use golangci-lint and have a lot of similar code:

    func foo() (object, error)
      err := doSomething()
      if err != nil {
        return nil, errors.Wrapf(err, "action failed for %v", anyAdditionalInfo)
      }
    }
    

    Starting from version golangci-lint 1.39.0 built from 9aea4aee on 2021-03-26T08:02:53Z all those lines cannot pass wrapcheck:

    foo/bar:42:16: error returned from external package is unwrapped (wrapcheck)
                             return nil, errors.Wrapf(err, "action failed for %v", anyAdditionalInfo)
    

    Is that a false positive error ? Or am I missing something ?

    Thanks.

  • false positive messages on legal errors from the same file

    false positive messages on legal errors from the same file

    file internal/modifier/modifiers_aggregate.go contains code:

    var (
        ErrPlaylistsNotTwo = errors.New("playlists not two")
    )
    
    if len(playlists) != 2 {
        return nil, ErrPlaylistsNotTwo
    }
    

    causes the following: internal/modifier/modifiers_aggregate.go:197:15: error returned from external package is unwrapped (wrapcheck) return nil, ErrPlaylistsNotTwo

  • Linter triggers on wrapper functions

    Linter triggers on wrapper functions

    Considering following code:

    // Deploy checks current status of deployed group of instances and updates them if there is some
    // configuration drift.
    func (a *apiLoadBalancers) Deploy() error {
      return a.containers.Deploy()
    }
    

    Right now linter triggers on it, but it seems very counter-intuitive.

    Used via golangci-lint version v1.39.0

  • Support Gorm-style error checking

    Support Gorm-style error checking

    Gorm uses non-standard error format to support it's chainable API. It would be nice if there was a way to configure wrapcheck to match a *.\.Error regex

    tx := tx.Find(&customer)
    if tx.Error != nil {
        return nil, tx.Error // wrapcheck should warn here
    }
    
    
    abc := tx.Find(&customer)
    if abc.Error != nil {
        return nil, abc.Error // wrapcheck should warn here
    }
    
  • Embedded interfaces are hard to ignore

    Embedded interfaces are hard to ignore

    When an interface is embedded, the reported interface name isn't what you need to ignore. Concretely, extend wrapcheck/testdata/config_ignoreInterfaceRegexps/main.go with the following, and run the test.

    type embedder interface {
    	errorer
    }
    
    func embed(fn embedder) error {
    	var str string
    	return fn.Decode(&str) // errorer interface ignored as per `ignoreInterfaceRegexps`
    }
    

    Unexpectedly, you'll see the following:

    --- FAIL: TestAnalyzer (5.40s) --- FAIL: TestAnalyzer/config_ignoreInterfaceRegexps (0.25s) analysistest.go:452: /home/murman/src/wrapcheck/wrapcheck/testdata/config_ignoreInterfaceRegexps/main.go:33:9: unexpected diagnostic: error returned from interface method should be wrapped: sig: func (_/home/murman/src/wrapcheck/wrapcheck/testdata/config_ignoreInterfaceRegexps.errorer).Decode(v interface{}) error

    Note that the ignores include errorer and the reported interface is config_ignoreInterfaceRegexps.errorer. However to suppress this you actually need to suppress embedder.

    I believe this comes down to the difference between name and fnSig in reportUnwrapped, which come from sel.Sel and sel.X respectively, but the path forward is unclear to me. https://github.com/tomarrell/wrapcheck/blob/213318509af6a003be2be752da826d269149ba4d/wrapcheck/wrapcheck.go#L257-L260

  • Flag to ignore main module

    Flag to ignore main module

    As has been requested, ignoring the main module in a project via a flag to prevent having to configure the package ignore for each project.

    Something along the lines of ignore: go list -m.

  • False-positive for anonymous functions

    False-positive for anonymous functions

    Wrapcheck throws an error for the next case:

    import "errors"
    
    func Test_AnonFunc(t *testing.T) {
    	test := func() error {
    		return errors.New("test")
    	}
    	fmt.Println(test())
    }
    

    error returned from external package is unwrapped: sig: func errors.New(text string) error

  • Named return values not reported

    Named return values not reported

    Try this out

    package main
    
    import (
    	"errors"
    )
    
    func somethingDangerous() error {
    	return errors.New("fake err")
    }
    
    func run() (err error) {
    	err = somethingDangerous()
    	return
    }
    
    func main() {
    	if err := run(); err != nil {
    		println(err)
    	}
    }
    

    You'll see that wrap check does not detect that err is not being wrapped.

  • consider flagging errors.Wrap(err) where err is known to be nil

    consider flagging errors.Wrap(err) where err is known to be nil

    I found several cases in my codebase (https://github.com/kopia/kopia/pull/747) that follow the following buggy pattern:

    err := doSomething()
    if err != nil {
      return result, errors.Wrap(err, "something failed")
    }
    
    if somethingElse {
      return result, errors.Wrap(err, "something else failed")
    }
    

    The second errors.Wrap() is incorrect, because it's not wrapping any error (but it's easy to copy/paste it as such).

    This is quite hard to spot in code reviews because lines that return errors will always have errors.Wrap() and it looks ok at first glance, until you notice that in this case is err is always nil. Because errors.Wrap(nil, "something") always returns nil this one returns success, which is unintended.

    Based on flow analysis, it should be possible to determine that err == nil in this case, and it would be really amazing if the linter could flag this pattern is as misuse.

[mirror] This is a linter for Go source code.

Golint is a linter for Go source code. Installation Golint requires a supported release of Go. go get -u golang.org/x/lint/golint To find out where g

Dec 23, 2022
Staticcheck - The advanced Go linter

The advanced Go linter Staticcheck is a state of the art linter for the Go programming language. Using static analysis, it finds bugs and performance

Jan 1, 2023
A linter that handles struct tags.

Tagliatelle A linter that handles struct tags. Supported string casing: camel pascal kebab snake goCamel Respects Go's common initialisms (e.g. HttpRe

Dec 15, 2022
a simple golang SSA viewer tool use for code analysis or make a linter
a simple golang SSA viewer tool use for code analysis or make a linter

ssaviewer A simple golang SSA viewer tool use for code analysis or make a linter ssa.html generate code modify from src/cmd/compile/internal/ssa/html.

May 17, 2022
The Golang linter that checks that there is no simultaneous return of `nil` error and an invalid value.

nilnil Checks that there is no simultaneous return of nil error and an invalid value. Installation & usage $ go install github.com/Antonboom/nilnil@la

Dec 14, 2022
Go linter which checks for dangerous unicode character sequences

bidichk - checks for dangerous unicode character sequences bidichk finds dangerous unicode character sequences in Go source files. Considered dangerou

Oct 5, 2022
Go linter that checks types that are json encoded - reports unsupported types and unnecessary error checks

Checks types passed to the json encoding functions. Reports unsupported types and reports occations, where the check for the returned error can be omited.

Oct 7, 2022
Linter for PostgreSQL

Использование Проверить миграции: oh-my-pg-linter check ./migrations/*.sql Добавить директории с дополнительными проверками (переопределение - кто пос

Nov 25, 2021
containedctx detects is a linter that detects struct contained context.Context field

containedctx containedctx detects is a linter that detects struct contained context.Context field Instruction go install github.com/sivchari/contained

Oct 22, 2022
World's spookiest linter

nosleep The world's spookiest linter nosleep is a golang-ci compatible linter which checks for and fails if it detects usages of time.Sleep. Why did y

Oct 15, 2022
Go linter to analyze expression groups: require 'import' declaration groups

grouper — a Go linter to analyze expression groups Installation

Jun 19, 2022
funcresult — a Go linter to analyze function result parameters

Go linter to analyze function result parameters: require named / unnamed function result parameters

Jan 27, 2022
nostdglobals is a simple Go linter that checks for usages of global variables defined in the go standard library

nostdglobals is a simple Go linter that checks for usages of global variables defined in the go standard library

Feb 17, 2022
Goalinter-v1: Goa framework (version1) linter

goavl: Goa framework (ver1) linter goavlは、goa version1(フォーク版)のlinterです。開発目的は、goa

Jul 28, 2022
Linter for Go's fmt.Errorf message

wrapmsg wrapmsg is Go code linter. this enforces fmt.Errorf's message when you wrap error. Example // OK ???? if err := pkg.Cause(); err != nil { re

Dec 27, 2022
misspelled word linter for Go comments, string literals and embedded files

gospel The gospel program lints Go source files for misspellings in comments, strings and embedded files. It uses hunspell to identify misspellings an

Aug 6, 2022
errcheck checks that you checked errors.

errcheck errcheck is a program for checking for unchecked errors in go programs. Install go get -u github.com/kisielk/errcheck errcheck requires Go 1

Jan 1, 2023
Clean architecture validator for go, like a The Dependency Rule and interaction between packages in your Go projects.
Clean architecture validator for go, like a The Dependency Rule and interaction between packages in your Go projects.

Clean Architecture checker for Golang go-cleanarch was created to keep Clean Architecture rules, like a The Dependency Rule and interaction between mo

Dec 31, 2022
Find outdated golang packages
Find outdated golang packages

This project is not supported anymore Go-outdated is minimalistic library that helps to find outdated packages hosted on github.com in your golang pro

Sep 27, 2022