• Stars
    star
    299
  • Rank 138,924 (Top 3 %)
  • Language
    Go
  • License
    MIT License
  • Created about 4 years ago
  • Updated about 2 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

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

Wrapcheck

Go Report Card Tests

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(
- errors.Join(
- .Wrap(
- .Wrapf(
- .WithMessage(
- .WithMessagef(
- .WithStack(

# An array of strings which specify regular expressions of signatures to ignore.
# This is similar to the ignoreSigs configuration above, but gives slightly more
# flexibility.
ignoreSigRegexps:
- \.New.*Error\(

# An array of glob patterns which, if any match the package of the function
# returning the error, will skip wrapcheck analysis for this error. This is
# useful for broadly ignoring packages and/or subpackages from wrapcheck
# analysis. There are no defaults for this value.
ignorePackageGlobs:
- encoding/*
- github.com/pkg/*

# ignoreInterfaceRegexps defines a list of regular expressions which, if matched
# to a underlying interface name, will ignore unwrapped errors returned from a
# function whose call is defined on the given interface.
ignoreInterfaceRegexps:
- ^(?i)c(?-i)ach(ing|e)

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.

More Repositories

1

lbadd

LBADD: An experimental, distributed SQL database
Go
387
star
2

miniqueue

A simple, single binary, message queue. Supports HTTP/2 and Redis Protocol.
Go
207
star
3

rust-elias-fano

Elias-Fano encoding implementation in Rust
Rust
28
star
4

vim-npr

Sensible 'gf' for Node Path Relative JS module resolution per project 🐿
Vim Script
23
star
5

rasp-ws2812b

Controlling WS2812b LED's using Rust on the Raspberry Pi
Rust
17
star
6

snake

Parallel, abstract Snake engine 🐍
Go
9
star
7

poker_tracker_api

A Poker tracking API written in Go
Go
7
star
8

talks

Slides and notes for talks that I've given
Go
7
star
9

generative

Generative art experiments
Go
7
star
10

poker_tracker_ui

React UI for Poker Tracker App
JavaScript
6
star
11

R2D2

React HOC for ensuring data from store, otherwise dispatching for it
JavaScript
5
star
12

ChangeCMS

Change - A lightweight NodeJS CMS platform built with Meteor
CSS
5
star
13

rmraf

Play with fire, and your root directory's existence!
JavaScript
5
star
14

tools

Personal tooling configuration
Vim Script
4
star
15

gif_emoji

Turn images into animated rotating gifs
Python
4
star
16

tomarrellcom

Old personal site source. Includes interactive snake game written in vanilla JS working on both desktop and mobile.
CSS
4
star
17

React-Express-Skeleton

A Simple React + Express Application Skeleton
JavaScript
4
star
18

website

Personal website, ab 2019
JavaScript
3
star
19

blog

Personal blog, barely coherent
Go
3
star
20

sumup-lessons

Repo containing information for the Go programming lessons run at SumUp
Go
3
star
21

react-media-rtc

A lightweight WebRTC library for React apps
JavaScript
2
star
22

Date-Cake

A natural-language date calculator
JavaScript
2
star
23

tre

A simple, fast and interactive alternative to 'tree'
Rust
2
star
24

tomarrell

Add README.md to my GitHub profile
1
star
25

vim8

Vim8 specific dotfiles
Vim Script
1
star
26

rust-hands-on

Repo containing code used at the Rust functional programming hands-on meetup
Rust
1
star
27

SkiSim

A ski-field simulation with GUI written in Java including fully implemented multi-threading.
Java
1
star
28

MeteorTodo

Meteor Todo Application
JavaScript
1
star
29

tomarrell.co

My personal website and development blog.
CSS
1
star
30

BankErrorChecking

Project for Thales NZ. Checks for errors/inconsistencies in reports sent and received from a bank.
Python
1
star
31

dotfiles

Emacs configuration. Forked from @d4ncer
Emacs Lisp
1
star
32

cms-experiment

CSS
1
star
33

redcli

A simple Redis client for Pub/Sub
Go
1
star
34

multitouch

Go
1
star
35

plantower

A library for decoding data from the Plantower series of sensors
Go
1
star
36

o_f

Go
1
star
37

dots

Personally written & maintained custom dotfiles
Emacs Lisp
1
star
38

poker-v2

Poker Tracker reborn, in Rust
Rust
1
star
39

rust-cli-table

A CLI tabling tool which parses CSVs - Rust
Rust
1
star
40

Email-Scraper

Python
1
star
41

infra

Personal infrastructure manifests
Shell
1
star
42

emu

A tool to help manage .env files per project
Rust
1
star
43

photos

Documenting my personal photo backup strategy
1
star
44

loki

Like Prometheus, but for logs.
Go
1
star