Repository: mbrt/gmailctl
Pull request #103 (2020-04-13 20:41:48)

Better errors


Patch:
diff --git a/cmd/gmailctl/cmd/apply_cmd.go b/cmd/gmailctl/cmd/apply_cmd.go
index d34a437..1492f8a 100644
--- a/cmd/gmailctl/cmd/apply_cmd.go
+++ b/cmd/gmailctl/cmd/apply_cmd.go
@@ -3,10 +3,10 @@ package cmd
 import (
 	"fmt"
 
-	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 
 	papply "github.com/mbrt/gmailctl/pkg/apply"
+	"github.com/mbrt/gmailctl/pkg/errors"
 )
 
 var (
@@ -59,7 +59,7 @@ func apply(path string, interactive, test bool) error {
 
 	gmailapi, err := openAPI()
 	if err != nil {
-		return configurationError(errors.Wrap(err, "cannot connect to Gmail"))
+		return configurationError(fmt.Errorf("cannot connect to Gmail: %w", err))
 	}
 
 	upstream, err := upstreamConfig(gmailapi)
@@ -69,7 +69,7 @@ func apply(path string, interactive, test bool) error {
 
 	diff, err := papply.Diff(parseRes.Res.GmailConfig, upstream)
 	if err != nil {
-		return errors.Wrap(err, "cannot compare upstream with local config")
+		return fmt.Errorf("cannot compare upstream with local config: %w", err)
 	}
 
 	if diff.Empty() {
@@ -86,7 +86,7 @@ func apply(path string, interactive, test bool) error {
 	if len(diff.LabelsDiff.Removed) > 0 {
 		fmt.Println(renameLabelWarning)
 		if !applyRemoveLabels {
-			return UserError(errors.New("no changes have been made"),
+			return errors.WithDetails(errors.New("no changes have been made"),
 				"To protect you, deletion is disabled unless you\n"+
 					"explicitly provide the --remove-labels flag.\n")
 		}
@@ -101,5 +101,5 @@ func apply(path string, interactive, test bool) error {
 }
 
 func configurationError(err error) error {
-	return UserError(err, "The configuration can be initialized with 'gmailctl init'")
+	return errors.WithDetails(err, "The configuration can be initialized with 'gmailctl init'")
 }
diff --git a/cmd/gmailctl/cmd/config.go b/cmd/gmailctl/cmd/config.go
index ffe54b7..ef804ac 100644
--- a/cmd/gmailctl/cmd/config.go
+++ b/cmd/gmailctl/cmd/config.go
@@ -1,11 +1,11 @@
 package cmd
 
 import (
+	"errors"
+	"fmt"
 	"os"
 	"path"
 
-	"github.com/pkg/errors"
-
 	papply "github.com/mbrt/gmailctl/pkg/apply"
 	"github.com/mbrt/gmailctl/pkg/cfgtest"
 	"github.com/mbrt/gmailctl/pkg/config"
@@ -31,10 +31,10 @@ func parseConfig(path, originalPath string, test bool) (parseResult, error) {
 
 	res.Config, err = config.ReadFile(path, originalPath)
 	if err != nil {
-		if config.IsNotFound(err) {
+		if errors.Is(err, config.ErrNotFound) {
 			return res, configurationError(err)
 		}
-		return res, errors.Wrap(err, "syntax error in config file")
+		return res, fmt.Errorf("syntax error in config file: %w", err)
 	}
 
 	if res.Config.Version != config.LatestVersion {
@@ -55,10 +55,11 @@ func parseConfig(path, originalPath string, test bool) (parseResult, error) {
 			for _, err := range errs {
 				stderrPrintf("  %v\n", err)
 			}
+			stderrPrintf("\n")
 		}
 		err = ts.ExecTests(res.Config.Tests)
 		if err != nil {
-			return res, errors.Wrap(err, "config tests failed")
+			return res, fmt.Errorf("config tests failed: %w", err)
 		}
 	}
 
diff --git a/cmd/gmailctl/cmd/debug_cmd.go b/cmd/gmailctl/cmd/debug_cmd.go
index 5710ff3..38a2141 100644
--- a/cmd/gmailctl/cmd/debug_cmd.go
+++ b/cmd/gmailctl/cmd/debug_cmd.go
@@ -4,7 +4,6 @@ import (
 	"fmt"
 	"net/url"
 
-	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 	"gopkg.in/yaml.v2"
 
@@ -52,7 +51,7 @@ func debug(path string) error {
 	parsedRules := parseRes.Res.Rules
 
 	if exp, got := len(configRules), len(parsedRules); exp != got {
-		return errors.Errorf(
+		return fmt.Errorf(
 			"unexpected number of generated rules: got %d, expected %d",
 			got, exp,
 		)
@@ -62,7 +61,7 @@ func debug(path string) error {
 		parsed := parsedRules[i]
 		criteria, err := filter.GenerateCriteria(parsed.Criteria)
 		if err != nil {
-			return errors.Wrap(err, "error generating criteria")
+			return fmt.Errorf("generating criteria: %w", err)
 		}
 		search := criteria.ToGmailSearch()
 
@@ -71,7 +70,7 @@ func debug(path string) error {
 		cfg := parsedRules[i]
 		b, err := yaml.Marshal(cfg)
 		if err != nil {
-			return errors.Wrap(err, "error marshalling rule")
+			return fmt.Errorf("marshalling rule: %w", err)
 		}
 		fmt.Println(string(b))
 	}
diff --git a/cmd/gmailctl/cmd/diff_cmd.go b/cmd/gmailctl/cmd/diff_cmd.go
index ce0375a..355daad 100644
--- a/cmd/gmailctl/cmd/diff_cmd.go
+++ b/cmd/gmailctl/cmd/diff_cmd.go
@@ -3,7 +3,6 @@ package cmd
 import (
 	"fmt"
 
-	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 
 	papply "github.com/mbrt/gmailctl/pkg/apply"
@@ -48,7 +47,7 @@ func diff(path string) error {
 
 	gmailapi, err := openAPI()
 	if err != nil {
-		return configurationError(errors.Wrap(err, "cannot connect to Gmail"))
+		return configurationError(fmt.Errorf("cannot connect to Gmail: %w", err))
 	}
 
 	upstream, err := upstreamConfig(gmailapi)
@@ -58,7 +57,7 @@ func diff(path string) error {
 
 	diff, err := papply.Diff(parseRes.Res.GmailConfig, upstream)
 	if err != nil {
-		return errors.Wrap(err, "cannot compare upstream with local config")
+		return fmt.Errorf("cannot compare upstream with local config: %w", err)
 	}
 
 	fmt.Print(diff)
diff --git a/cmd/gmailctl/cmd/download_cmd.go b/cmd/gmailctl/cmd/download_cmd.go
index 3d45fb9..0f62a27 100644
--- a/cmd/gmailctl/cmd/download_cmd.go
+++ b/cmd/gmailctl/cmd/download_cmd.go
@@ -4,11 +4,11 @@ import (
 	"bufio"
 	"bytes"
 	"encoding/json"
+	"fmt"
 	"io"
 	"os"
 	"regexp"
 
-	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 
 	"github.com/mbrt/gmailctl/pkg/rimport"
@@ -70,7 +70,7 @@ func download(outputPath string) (err error) {
 	} else {
 		f, e := os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
 		if e != nil {
-			return errors.Wrap(err, "cannot open output")
+			return fmt.Errorf("opening output: %w", err)
 		}
 		defer func() {
 			e = f.Close()
@@ -87,7 +87,7 @@ func download(outputPath string) (err error) {
 func downloadWithOut(out io.Writer) error {
 	gmailapi, err := openAPI()
 	if err != nil {
-		return configurationError(errors.Wrap(err, "cannot connect to Gmail"))
+		return configurationError(fmt.Errorf("connecting to Gmail: %w", err))
 	}
 
 	upstream, err := upstreamConfig(gmailapi)
@@ -102,7 +102,7 @@ func downloadWithOut(out io.Writer) error {
 
 	err = marshalJsonnet(cfg, out)
 	if err != nil {
-		return errors.Wrap(err, "error converting to Jsonnet")
+		return fmt.Errorf("converting to Jsonnet: %w", err)
 	}
 	return nil
 }
diff --git a/cmd/gmailctl/cmd/edit_cmd.go b/cmd/gmailctl/cmd/edit_cmd.go
index 6cfe7af..2655a73 100644
--- a/cmd/gmailctl/cmd/edit_cmd.go
+++ b/cmd/gmailctl/cmd/edit_cmd.go
@@ -8,12 +8,12 @@ import (
 	"path/filepath"
 	"strings"
 
-	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 
 	"github.com/mbrt/gmailctl/pkg/api"
 	papply "github.com/mbrt/gmailctl/pkg/apply"
 	"github.com/mbrt/gmailctl/pkg/config"
+	"github.com/mbrt/gmailctl/pkg/errors"
 )
 
 // Parameters
@@ -77,7 +77,7 @@ func edit(path string, test bool) error {
 	// applied now.
 	gmailapi, err := openAPI()
 	if err != nil {
-		return configurationError(errors.Wrap(err, "cannot connect to Gmail"))
+		return configurationError(fmt.Errorf("connecting to Gmail: %w", err))
 	}
 
 	// Copy the configuration in a temporary file and edit it.
@@ -94,20 +94,20 @@ func edit(path string, test bool) error {
 			return err
 		}
 		if err = applyEdited(tmpPath, path, test, gmailapi); err != nil {
-			if errors.Cause(err) == errUnchanged {
+			if errors.Is(err, errUnchanged) {
 				// Unchanged, but move the file anyways (it could be a refactoring)
 				return moveFile(tmpPath, path)
 			}
-			if errors.Cause(err) == errAbort {
-				return UserError(err, fmt.Sprintf(abortHelp, tmpPath))
+			if errors.Is(err, errAbort) {
+				return errors.WithDetails(err, fmt.Sprintf(abortHelp, tmpPath))
 			}
-			if errors.Cause(err) == errRetry {
+			if errors.Is(err, errRetry) {
 				continue
 			}
 
 			stderrPrintf("Error applying configuration: %v\n", err)
 			if !askYN("Do you want to continue editing?") {
-				return UserError(errAbort, fmt.Sprintf(abortHelp, tmpPath))
+				return errors.WithDetails(errAbort, fmt.Sprintf(abortHelp, tmpPath))
 			}
 			// Retry
 			continue
@@ -140,13 +140,13 @@ func moveFile(from, to string) error {
 func copyToTmp(path string) (string, error) {
 	b, err := ioutil.ReadFile(path)
 	if err != nil {
-		return "", config.NotFoundError(err)
+		return "", errors.WithCause(err, config.ErrNotFound)
 	}
 
 	// Use the same extension as the original file (yaml | jsonnet)
 	tmp, err := ioutil.TempFile("", fmt.Sprintf("gmailctl-*%s", filepath.Ext(path)))
 	if err != nil {
-		return "", errors.Wrap(err, "cannot create tmp file")
+		return "", fmt.Errorf("creating tmp file: %w", err)
 	}
 
 	if _, err := tmp.Write(b); err != nil {
@@ -198,7 +198,7 @@ func applyEdited(path, originalPath string, test bool, gmailapi *api.GmailAPI) e
 
 	diff, err := papply.Diff(parseRes.Res.GmailConfig, upstream)
 	if err != nil {
-		return errors.New("cannot compare upstream with local config")
+		return errors.New("comparing upstream with local config")
 	}
 
 	if diff.Empty() {
diff --git a/cmd/gmailctl/cmd/errors.go b/cmd/gmailctl/cmd/errors.go
deleted file mode 100644
index 5c734eb..0000000
--- a/cmd/gmailctl/cmd/errors.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package cmd
-
-import "github.com/pkg/errors"
-
-// HasUserHelp returns true if an error has a help message for the user.
-func HasUserHelp(err error) bool {
-	uErr, ok := errors.Cause(err).(userHelp)
-	return ok && uErr.Help() != ""
-}
-
-// UserError wraps the given error and makes it into a not found one
-func UserError(err error, help string) error {
-	if err == nil {
-		return nil
-	}
-	return userError{err, help}
-}
-
-// GetUserHelp returns the user help associated with an error.
-func GetUserHelp(err error) string {
-	uErr, ok := errors.Cause(err).(userHelp)
-	if ok {
-		return uErr.Help()
-	}
-	return ""
-}
-
-type userHelp interface {
-	Help() string
-}
-
-type userError struct {
-	error
-	help string
-}
-
-func (e userError) Error() string { return e.error.Error() }
-func (e userError) Help() string  { return e.help }
diff --git a/cmd/gmailctl/cmd/export_cmd.go b/cmd/gmailctl/cmd/export_cmd.go
index 7aaef16..0ac5c5a 100644
--- a/cmd/gmailctl/cmd/export_cmd.go
+++ b/cmd/gmailctl/cmd/export_cmd.go
@@ -1,10 +1,10 @@
 package cmd
 
 import (
+	"fmt"
 	"io"
 	"os"
 
-	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 
 	"github.com/mbrt/gmailctl/pkg/export/xml"
@@ -53,7 +53,7 @@ func export(inputPath, outputPath string, test bool) (err error) {
 	} else {
 		f, e := os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
 		if e != nil {
-			return errors.Wrap(err, "cannot open output")
+			return fmt.Errorf("opening output: %w", err)
 		}
 		defer func() {
 			e = f.Close()
diff --git a/cmd/gmailctl/cmd/init_cmd.go b/cmd/gmailctl/cmd/init_cmd.go
index 2c807d0..4658e87 100644
--- a/cmd/gmailctl/cmd/init_cmd.go
+++ b/cmd/gmailctl/cmd/init_cmd.go
@@ -6,7 +6,6 @@ import (
 	"os"
 	"path"
 
-	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 
 	"github.com/mbrt/gmailctl/pkg/api"
@@ -90,7 +89,7 @@ func resetConfig() error {
 
 func continueConfig() error {
 	if err := handleCfgDir(); err != nil {
-		return errors.Wrap(err, "error configuring the main config directory")
+		return fmt.Errorf("configuring the main config directory: %w", err)
 	}
 	auth, err := openCredentials()
 	if err != nil {
@@ -169,11 +168,11 @@ func setupToken(auth *api.Authenticator) error {
 
 	var authCode string
 	if _, err := fmt.Scan(&authCode); err != nil {
-		return errors.Wrap(err, "unable to retrieve token from web")
+		return fmt.Errorf("unable to retrieve token from web: %w", err)
 	}
 
 	if err := saveToken(tokenPath, authCode, auth); err != nil {
-		return errors.Wrap(err, "unable to cache token")
+		return fmt.Errorf("caching token: %w", err)
 	}
 	return nil
 }
@@ -182,7 +181,7 @@ func saveToken(path, authCode string, auth *api.Authenticator) (err error) {
 	fmt.Printf("Saving credential file to %s\n", path)
 	f, e := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
 	if e != nil {
-		return errors.Wrap(err, "unable create token file")
+		return fmt.Errorf("creating token file: %w", err)
 	}
 	defer func() {
 		e = f.Close()
diff --git a/cmd/gmailctl/cmd/initapi.go b/cmd/gmailctl/cmd/initapi.go
index e0d10c0..bf9e5a5 100644
--- a/cmd/gmailctl/cmd/initapi.go
+++ b/cmd/gmailctl/cmd/initapi.go
@@ -2,17 +2,16 @@ package cmd
 
 import (
 	"context"
+	"fmt"
 	"os"
 
-	"github.com/pkg/errors"
-
 	"github.com/mbrt/gmailctl/pkg/api"
 )
 
 func openAPI() (*api.GmailAPI, error) {
 	auth, err := openCredentials()
 	if err != nil {
-		return nil, errors.Wrap(err, "invalid credentials")
+		return nil, fmt.Errorf("invalid credentials: %w", err)
 	}
 	return openToken(auth)
 }
@@ -20,7 +19,7 @@ func openAPI() (*api.GmailAPI, error) {
 func openCredentials() (*api.Authenticator, error) {
 	cred, err := os.Open(credentialsPath)
 	if err != nil {
-		return nil, errors.Wrap(err, "cannot open credentials")
+		return nil, fmt.Errorf("opening credentials: %w", err)
 	}
 	return api.NewAuthenticator(cred)
 }
@@ -28,7 +27,7 @@ func openCredentials() (*api.Authenticator, error) {
 func openToken(auth *api.Authenticator) (*api.GmailAPI, error) {
 	token, err := os.Open(tokenPath)
 	if err != nil {
-		return nil, errors.Wrap(err, "missing or invalid cached token")
+		return nil, fmt.Errorf("missing or invalid cached token: %w", err)
 	}
 
 	return auth.API(context.Background(), token)
diff --git a/cmd/gmailctl/cmd/io.go b/cmd/gmailctl/cmd/io.go
index 5dc5c6a..f69df2f 100644
--- a/cmd/gmailctl/cmd/io.go
+++ b/cmd/gmailctl/cmd/io.go
@@ -5,6 +5,8 @@ import (
 	"fmt"
 	"os"
 	"strings"
+
+	"github.com/mbrt/gmailctl/pkg/errors"
 )
 
 func askYN(prompt string) bool {
@@ -55,9 +57,9 @@ func askOptions(prompt string, choices []string) int {
 }
 
 func fatal(err error) {
-	stderrPrintf("%v\n", err)
-	if HasUserHelp(err) {
-		stderrPrintf("\nNote: %s\n", GetUserHelp(err))
+	stderrPrintf("Error: %v\n", err)
+	if det := errors.Details(err); det != "" {
+		stderrPrintf("\nNote: %s\n", det)
 	}
 	os.Exit(1)
 }
diff --git a/cmd/gmailctl/cmd/upstream.go b/cmd/gmailctl/cmd/upstream.go
index 97379a6..351e74d 100644
--- a/cmd/gmailctl/cmd/upstream.go
+++ b/cmd/gmailctl/cmd/upstream.go
@@ -1,7 +1,7 @@
 package cmd
 
 import (
-	"github.com/pkg/errors"
+	"fmt"
 
 	"github.com/mbrt/gmailctl/pkg/api"
 	papply "github.com/mbrt/gmailctl/pkg/apply"
@@ -11,7 +11,7 @@ func upstreamConfig(gmailapi *api.GmailAPI) (papply.GmailConfig, error) {
 	f, err := gmailapi.ListFilters()
 	if err != nil {
 		if len(f) == 0 {
-			return papply.GmailConfig{}, errors.Wrap(err, "cannot get filters from Gmail")
+			return papply.GmailConfig{}, fmt.Errorf("getting filters from Gmail: %w", err)
 		}
 		// We have some filters, let's work with what we have and issue a warning.
 		stderrPrintf("Warning: Error getting one or more filters from Gmail: %sThey will be ignored in the diff.\n", err)
diff --git a/go.mod b/go.mod
index 10c0ce7..26a376d 100644
--- a/go.mod
+++ b/go.mod
@@ -7,12 +7,9 @@ require (
 	github.com/cnf/structhash v0.0.0-20180104161610-62a607eb0224
 	github.com/cpmech/gosl v1.1.1
 	github.com/go-bindata/go-bindata v3.1.2+incompatible // indirect
-	github.com/golang/protobuf v1.3.2 // indirect
 	github.com/google/go-jsonnet v0.15.0
 	github.com/hashicorp/go-multierror v1.1.0
-	github.com/kr/pretty v0.1.0 // indirect
 	github.com/mitchellh/go-homedir v1.1.0
-	github.com/pkg/errors v0.9.1
 	github.com/pmezard/go-difflib v1.0.0
 	github.com/spf13/cobra v0.0.7
 	github.com/stretchr/testify v1.5.1
@@ -20,6 +17,5 @@ require (
 	golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
 	golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // indirect
 	google.golang.org/api v0.21.0
-	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
 	gopkg.in/yaml.v2 v2.2.8
 )
diff --git a/go.sum b/go.sum
index e11eb8e..5c0a5ad 100644
--- a/go.sum
+++ b/go.sum
@@ -20,15 +20,11 @@ github.com/cnf/structhash v0.0.0-20180104161610-62a607eb0224 h1:rnCKRrdSBqc061l0
 github.com/cnf/structhash v0.0.0-20180104161610-62a607eb0224/go.mod h1:pCxVEbcm3AMg7ejXyorUXi6HQCzOIBf7zEDVPtw0/U4=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
-github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
-github.com/cpmech/gosl v1.1.0 h1:DDZqR/wvYp65P+busRIpb4DQr7KKDmYTw+f26DZL1eY=
-github.com/cpmech/gosl v1.1.0/go.mod h1:arn/jy2eYwkioG2eNbAW60SdPbJzNaHbqKmJYDnCpfs=
 github.com/cpmech/gosl v1.1.1 h1:FiKxlgyLnY6Kke3F0ojMfzpNiLC2Ag4xtAlCW5cgBdU=
 github.com/cpmech/gosl v1.1.1/go.mod h1:arn/jy2eYwkioG2eNbAW60SdPbJzNaHbqKmJYDnCpfs=
-github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -41,6 +37,7 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-bindata/go-bindata v1.0.0 h1:DZ34txDXWn1DyWa+vQf7V9ANc2ILTtrEjtlsdJRF26M=
 github.com/go-bindata/go-bindata v3.1.2+incompatible h1:5vjJMVhowQdPzjE1LdxyFF7YFTXg5IgGVW4gBr5IbvE=
 github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -64,8 +61,6 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-jsonnet v0.13.0 h1:Ul0FtJiQl705JIyGKaBZug/W2LBY5p0xwY08Q69eOAg=
-github.com/google/go-jsonnet v0.13.0/go.mod h1:gNwctc8xrpXNs749bjRLO58rjIBVrWz+pgsRoOCh5Vs=
 github.com/google/go-jsonnet v0.15.0 h1:lEUXTDnVsHu+CLLzMeWAdWV4JpCgkJeDqdVNS8RtyuY=
 github.com/google/go-jsonnet v0.15.0/go.mod h1:ex9QcU8vzXQUDeNe4gaN1uhGQbTYpOeZ6AbWdy6JbX4=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
@@ -80,8 +75,6 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
 github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
 github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
-github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
 github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
 github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -114,10 +107,6 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
-github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@@ -131,7 +120,6 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
-github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
@@ -141,16 +129,11 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
-github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
-github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs=
-github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
 github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU=
 github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
 github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
 github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
@@ -163,7 +146,6 @@ github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
-github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@@ -174,7 +156,6 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -216,7 +197,6 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -250,8 +230,6 @@ golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgw
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0 h1:9sdfJOzWlkqPltHAuzT2Cp+yrBeY1KRVYgms8soxMwM=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
-google.golang.org/api v0.20.0 h1:jz2KixHX7EcCPiQrySzPdnYT7DbINAypCqKZ1Z7GM40=
-google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
 google.golang.org/api v0.21.0 h1:zS+Q/CJJnVlXpXQVIz+lH0ZT2lBuT2ac7XD8Y/3w6hY=
 google.golang.org/api v0.21.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
 google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=
@@ -266,16 +244,12 @@ google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRn
 google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190626174449-989357319d63/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
-google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532 h1:5pOB7se0B2+IssELuQUs6uoBgYJenkU2AQlvopc2sRw=
-google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw=
-google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
diff --git a/pkg/api/api.go b/pkg/api/api.go
index c5cb529..58cc000 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -3,7 +3,6 @@ package api
 import (
 	"fmt"
 
-	"github.com/pkg/errors"
 	gmailv1 "google.golang.org/api/gmail/v1"
 
 	exportapi "github.com/mbrt/gmailctl/pkg/export/api"
@@ -41,7 +40,7 @@ func (g *GmailAPI) DeleteFilters(ids []string) error {
 	for _, id := range ids {
 		err := g.service.Users.Settings.Filters.Delete(gmailUser, id).Do()
 		if err != nil {
-			return errors.Wrap(err, fmt.Sprintf("error deleting filter '%s'", id))
+			return fmt.Errorf("deleting filter %q: %w", id, err)
 		}
 	}
 	return nil
@@ -62,7 +61,7 @@ func (g *GmailAPI) AddFilters(fs filter.Filters) error {
 	for i, gfilter := range gfilters {
 		_, err = g.service.Users.Settings.Filters.Create(gmailUser, gfilter).Do()
 		if err != nil {
-			return errors.Wrapf(err, "error creating filter '%d'", i)
+			return fmt.Errorf("creating filter %d: %w", i, err)
 		}
 	}
 
@@ -107,7 +106,7 @@ func (g *GmailAPI) DeleteLabels(ids []string) error {
 	for _, id := range ids {
 		err := g.service.Users.Labels.Delete(gmailUser, id).Do()
 		if err != nil {
-			return errors.Wrapf(err, "error deleting label '%s'", id)
+			return fmt.Errorf("deleting label %q: %w", id, err)
 		}
 	}
 	return nil
@@ -119,7 +118,7 @@ func (g *GmailAPI) AddLabels(lbs label.Labels) error {
 	for _, lb := range lbs {
 		_, err := g.service.Users.Labels.Create(gmailUser, labelToGmailAPI(lb)).Do()
 		if err != nil {
-			return errors.Wrapf(err, "error creating label '%s'", lb.Name)
+			return fmt.Errorf("creating label %q: %w", lb.Name, err)
 		}
 	}
 	return nil
@@ -131,11 +130,11 @@ func (g *GmailAPI) AddLabels(lbs label.Labels) error {
 func (g *GmailAPI) UpdateLabels(lbs label.Labels) error {
 	for _, lb := range lbs {
 		if lb.ID == "" {
-			return errors.Errorf("error, label '%s' has empty ID", lb.Name)
+			return fmt.Errorf("label %q has empty ID", lb.Name)
 		}
 		_, err := g.service.Users.Labels.Patch(gmailUser, lb.ID, labelToGmailAPI(lb)).Do()
 		if err != nil {
-			return errors.Wrapf(err, "error patching label '%s'", lb.Name)
+			return fmt.Errorf("patching label %q: %w", lb.Name, err)
 		}
 	}
 	return nil
diff --git a/pkg/api/auth.go b/pkg/api/auth.go
index 6f9144e..fc2d8e1 100644
--- a/pkg/api/auth.go
+++ b/pkg/api/auth.go
@@ -3,15 +3,14 @@ package api
 import (
 	"context"
 	"encoding/json"
+	"fmt"
 	"io"
 	"io/ioutil"
 
 	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/google"
-	"google.golang.org/api/option"
-
-	"github.com/pkg/errors"
 	gmailv1 "google.golang.org/api/gmail/v1"
+	"google.golang.org/api/option"
 )
 
 // NewAuthenticator creates an Authenticator instance from credentials JSON file contents.
@@ -21,7 +20,7 @@ import (
 func NewAuthenticator(credentials io.Reader) (*Authenticator, error) {
 	cfg, err := clientFromCredentials(credentials)
 	if err != nil {
-		return nil, errors.Wrap(err, "error creating config from credentials")
+		return nil, fmt.Errorf("creating config from credentials: %w", err)
 	}
 	return &Authenticator{cfg}, nil
 }
@@ -38,12 +37,12 @@ type Authenticator struct {
 func (a Authenticator) API(ctx context.Context, token io.Reader) (*GmailAPI, error) {
 	tok, err := parseToken(token)
 	if err != nil {
-		return nil, errors.Wrap(err, "error decoding token")
+		return nil, fmt.Errorf("decoding token: %w", err)
 	}
 
 	srv, err := gmailv1.NewService(ctx, option.WithTokenSource(a.cfg.TokenSource(ctx, tok)))
 	if err != nil {
-		return nil, errors.Wrap(err, "error creating gmail client")
+		return nil, fmt.Errorf("creating gmail client: %w", err)
 	}
 
 	return &GmailAPI{srv}, nil
@@ -61,7 +60,7 @@ func (a Authenticator) AuthURL() string {
 func (a Authenticator) CacheToken(ctx context.Context, authCode string, token io.Writer) error {
 	tok, err := a.cfg.Exchange(ctx, authCode)
 	if err != nil {
-		return errors.Wrap(err, "unable to retrieve token from web")
+		return fmt.Errorf("unable to retrieve token from web: %w", err)
 	}
 	return json.NewEncoder(token).Encode(tok)
 }
@@ -69,7 +68,7 @@ func (a Authenticator) CacheToken(ctx context.Context, authCode string, token io
 func clientFromCredentials(credentials io.Reader) (*oauth2.Config, error) {
 	credBytes, err := ioutil.ReadAll(credentials)
 	if err != nil {
-		return nil, errors.Wrap(err, "error reading credentials")
+		return nil, fmt.Errorf("reading credentials: %w", err)
 	}
 	return google.ConfigFromJSON(credBytes,
 		gmailv1.GmailSettingsBasicScope,
diff --git a/pkg/apply/apply.go b/pkg/apply/apply.go
index ab13c88..3aa4fc7 100644
--- a/pkg/apply/apply.go
+++ b/pkg/apply/apply.go
@@ -1,11 +1,10 @@
 package apply
 
 import (
+	"fmt"
 	"sort"
 	"strings"
 
-	"github.com/pkg/errors"
-
 	cfgv3 "github.com/mbrt/gmailctl/pkg/config/v1alpha3"
 	"github.com/mbrt/gmailctl/pkg/filter"
 	"github.com/mbrt/gmailctl/pkg/label"
@@ -31,11 +30,11 @@ func FromConfig(cfg cfgv3.Config) (ConfigParseRes, error) {
 
 	res.Rules, err = parser.Parse(cfg)
 	if err != nil {
-		return res, errors.Wrap(err, "cannot parse config file")
+		return res, fmt.Errorf("cannot parse config file: %w", err)
 	}
 	res.Filters, err = filter.FromRules(res.Rules)
 	if err != nil {
-		return res, errors.Wrap(err, "error exporting to filters")
+		return res, fmt.Errorf("exporting to filters: %w", err)
 	}
 	res.Labels = label.FromConfig(cfg.Labels)
 
@@ -79,10 +78,10 @@ func (d ConfigDiff) Validate() error {
 		return nil
 	}
 	if err := d.LocalConfig.Labels.Validate(); err != nil {
-		return errors.Wrap(err, "error validating labels")
+		return fmt.Errorf("validating labels: %w", err)
 	}
 	if err := label.Validate(d.LabelsDiff, d.LocalConfig.Filters); err != nil {
-		return errors.Wrap(err, "invalid labels diff")
+		return fmt.Errorf("invalid labels diff: %w", err)
 	}
 	return nil
 }
@@ -96,14 +95,14 @@ func Diff(local, upstream GmailConfig) (ConfigDiff, error) {
 
 	res.FiltersDiff, err = filter.Diff(upstream.Filters, local.Filters)
 	if err != nil {
-		return res, errors.Wrap(err, "cannot compute filters diff")
+		return res, fmt.Errorf("cannot compute filters diff: %w", err)
 	}
 
 	if len(local.Labels) > 0 {
 		// LabelsDiff management opted-in
 		res.LabelsDiff, err = label.Diff(upstream.Labels, local.Labels)
 		if err != nil {
-			return res, errors.Wrap(err, "cannot compute labels diff")
+			return res, fmt.Errorf("cannot compute labels diff: %w", err)
 		}
 	}
 
@@ -130,23 +129,23 @@ func Apply(d ConfigDiff, api API, allowRemoveLabels bool) error {
 	// - remove labels
 
 	if err := addLabels(d.LabelsDiff.Added, api); err != nil {
-		return errors.Wrap(err, "error creating labels")
+		return fmt.Errorf("creating labels: %w", err)
 	}
 	if err := addFilters(d.FiltersDiff.Added, api); err != nil {
-		return errors.Wrap(err, "error creating filters")
+		return fmt.Errorf("creating filters: %w", err)
 	}
 	if err := updateLabels(d.LabelsDiff.Modified, api); err != nil {
-		return errors.Wrap(err, "error updating labels")
+		return fmt.Errorf("updating labels: %w", err)
 	}
 	if err := removeFilters(d.FiltersDiff.Removed, api); err != nil {
-		return errors.Wrap(err, "error deleting filters")
+		return fmt.Errorf("deleting filters: %w", err)
 	}
 
 	if !allowRemoveLabels {
 		return nil
 	}
 	if err := removeLabels(d.LabelsDiff.Removed, api); err != nil {
-		return errors.Wrap(err, "error removing labels")
+		return fmt.Errorf("removing labels: %w", err)
 	}
 
 	return nil
diff --git a/pkg/config/read.go b/pkg/config/read.go
index de60acc..5c41aa1 100644
--- a/pkg/config/read.go
+++ b/pkg/config/read.go
@@ -3,21 +3,25 @@ package config
 import (
 	"bytes"
 	"encoding/json"
+	"fmt"
 	"io/ioutil"
 	"path/filepath"
 
 	"github.com/google/go-jsonnet"
-	"github.com/pkg/errors"
 	"gopkg.in/yaml.v2"
 
 	cfgv1 "github.com/mbrt/gmailctl/pkg/config/v1alpha1"
 	cfgv2 "github.com/mbrt/gmailctl/pkg/config/v1alpha2"
 	cfgv3 "github.com/mbrt/gmailctl/pkg/config/v1alpha3"
+	"github.com/mbrt/gmailctl/pkg/errors"
 )
 
 // LatestVersion points to the latest version of the config format.
 const LatestVersion = cfgv3.Version
 
+// ErrNotFound is returned when a file was not found.
+var ErrNotFound = errors.New("config not found")
+
 // ReadFile takes a path and returns the parsed config file.
 //
 // If the config file needs to have access to additional libraries,
@@ -26,7 +30,7 @@ func ReadFile(path, libPath string) (cfgv3.Config, error) {
 	/* #nosec */
 	b, err := ioutil.ReadFile(path)
 	if err != nil {
-		return cfgv3.Config{}, NotFoundError(err)
+		return cfgv3.Config{}, errors.WithCause(err, ErrNotFound)
 	}
 	if filepath.Ext(path) == ".jsonnet" {
 		// We pass the libPath to jsonnet, because that is the hint
@@ -45,11 +49,11 @@ func readJsonnet(path string, buf []byte) (cfgv3.Config, error) {
 	vm := jsonnet.MakeVM()
 	jstr, err := vm.EvaluateSnippet(path, string(buf))
 	if err != nil {
-		return res, errors.Wrap(err, "invalid jsonnet")
+		return res, fmt.Errorf("parsing jsonnet: %w", err)
 	}
 	version, err := readJSONVersion(jstr)
 	if err != nil {
-		return res, errors.Wrap(err, "error parsing the config version")
+		return res, fmt.Errorf("parsing the config version: %w", err)
 	}
 
 	switch version {
@@ -61,7 +65,7 @@ func readJsonnet(path string, buf []byte) (cfgv3.Config, error) {
 		var v2 cfgv2.Config
 		err = jsonUnmarshalStrict([]byte(jstr), &v2)
 		if err != nil {
-			return res, errors.Wrap(err, "error parsing v1alpha2 config")
+			return res, fmt.Errorf("parsing v1alpha2 config: %w", err)
 		}
 		return importFromV2(v2)
 
@@ -69,12 +73,12 @@ func readJsonnet(path string, buf []byte) (cfgv3.Config, error) {
 		var v1 cfgv1.Config
 		err = jsonUnmarshalStrict([]byte(jstr), &v1)
 		if err != nil {
-			return res, errors.Wrap(err, "error parsing v1alpha1 config")
+			return res, fmt.Errorf("parsing v1alpha1 config: %w", err)
 		}
 		return importFromV1(v1)
 
 	default:
-		return res, errors.Errorf("unknown config version: %s", version)
+		return res, fmt.Errorf("unknown config version: %s", version)
 	}
 }
 
@@ -83,7 +87,7 @@ func readYaml(buf []byte) (cfgv3.Config, error) {
 	var res cfgv3.Config
 	version, err := readYamlVersion(buf)
 	if err != nil {
-		return res, errors.Wrap(err, "error parsing the config version")
+		return res, fmt.Errorf("parsing the config version: %w", err)
 	}
 
 	switch version {
@@ -95,7 +99,7 @@ func readYaml(buf []byte) (cfgv3.Config, error) {
 		var v2 cfgv2.Config
 		err = yaml.UnmarshalStrict(buf, &v2)
 		if err != nil {
-			return res, errors.Wrap(err, "error parsing v1alpha2 config")
+			return res, fmt.Errorf("parsing v1alpha2 config: %w", err)
 		}
 		return importFromV2(v2)
 
@@ -103,12 +107,12 @@ func readYaml(buf []byte) (cfgv3.Config, error) {
 		var v1 cfgv1.Config
 		err = yaml.UnmarshalStrict(buf, &v1)
 		if err != nil {
-			return res, errors.Wrap(err, "error parsing v1alpha1 config")
+			return res, fmt.Errorf("parsing v1alpha1 config: %w", err)
 		}
 		return importFromV1(v1)
 
 	default:
-		return res, errors.Errorf("unknown config version: %s", version)
+		return res, fmt.Errorf("unknown config version: %s", version)
 	}
 }
 
@@ -147,28 +151,3 @@ func jsonUnmarshalStrict(b []byte, v interface{}) error {
 	dec.DisallowUnknownFields()
 	return dec.Decode(v)
 }
-
-// IsNotFound returns true if an error is related to a file not found
-func IsNotFound(err error) bool {
-	nfErr, ok := errors.Cause(err).(notFound)
-	return ok && nfErr.NotFound()
-}
-
-// NotFoundError wraps the given error and makes it into a not found one
-func NotFoundError(err error) error {
-	if err == nil {
-		return nil
-	}
-	return notFoundError{err}
-}
-
-type notFound interface {
-	NotFound() bool
-}
-
-type notFoundError struct {
-	error
-}
-
-func (e notFoundError) Error() string  { return e.error.Error() }
-func (e notFoundError) NotFound() bool { return true }
diff --git a/pkg/config/v1alpha1/resolve.go b/pkg/config/v1alpha1/resolve.go
index 2d3cc9b..aa62bdc 100644
--- a/pkg/config/v1alpha1/resolve.go
+++ b/pkg/config/v1alpha1/resolve.go
@@ -1,8 +1,6 @@
 package v1alpha1
 
-import (
-	"github.com/pkg/errors"
-)
+import "fmt"
 
 // ResolveConsts returns a copy of the config with all the constants
 // replaced in the filters.
@@ -12,7 +10,7 @@ func ResolveConsts(c Config) (Config, error) {
 	for i, r := range c.Rules {
 		f, err := resolveFilters(r.Filters, c.Consts)
 		if err != nil {
-			return c, errors.Wrapf(err, "error in rule #%d", i)
+			return c, fmt.Errorf("in rule #%d: %w", i, err)
 		}
 		rules = append(rules, Rule{f, r.Actions})
 	}
@@ -46,27 +44,27 @@ func resolveFilters(f Filters, consts Consts) (Filters, error) {
 func resolveFiltersConsts(mf MatchFilters, consts Consts) (MatchFilters, error) {
 	from, err := resolveConsts(mf.From, consts)
 	if err != nil {
-		return mf, errors.Wrap(err, "error in resolving 'from' clause")
+		return mf, fmt.Errorf("resolving 'from' clause: %w", err)
 	}
 	to, err := resolveConsts(mf.To, consts)
 	if err != nil {
-		return mf, errors.Wrap(err, "error in resolving 'to' clause")
+		return mf, fmt.Errorf("resolving 'to' clause: %w", err)
 	}
 	cc, err := resolveConsts(mf.Cc, consts)
 	if err != nil {
-		return mf, errors.Wrap(err, "error in resolving 'cc' clause")
+		return mf, fmt.Errorf("resolving 'cc' clause: %w", err)
 	}
 	sub, err := resolveConsts(mf.Subject, consts)
 	if err != nil {
-		return mf, errors.Wrap(err, "error in resolving 'subject' clause")
+		return mf, fmt.Errorf("resolving 'subject' clause: %w", err)
 	}
 	has, err := resolveConsts(mf.Has, consts)
 	if err != nil {
-		return mf, errors.Wrap(err, "error in resolving 'has' clause")
+		return mf, fmt.Errorf("resolving 'has' clause: %w", err)
 	}
 	list, err := resolveConsts(mf.List, consts)
 	if err != nil {
-		return mf, errors.Wrap(err, "error in resolving 'list' clause")
+		return mf, fmt.Errorf("resolving 'list' clause: %w", err)
 	}
 	res := MatchFilters{
 		From:    from,
@@ -84,7 +82,7 @@ func resolveConsts(a []string, consts Consts) ([]string, error) {
 	for _, s := range a {
 		resolved, ok := consts[s]
 		if !ok {
-			return nil, errors.Errorf("failed to resolve const '%s'", s)
+			return nil, fmt.Errorf("failed to resolve const '%s'", s)
 		}
 		res = append(res, resolved.Values...)
 	}
diff --git a/pkg/config/v1alpha2/import.go b/pkg/config/v1alpha2/import.go
index 3d5845d..8cbc0fa 100644
--- a/pkg/config/v1alpha2/import.go
+++ b/pkg/config/v1alpha2/import.go
@@ -1,7 +1,7 @@
 package v1alpha2
 
 import (
-	"github.com/pkg/errors"
+	"fmt"
 
 	v1 "github.com/mbrt/gmailctl/pkg/config/v1alpha1"
 )
@@ -10,7 +10,7 @@ import (
 func Import(cfg v1.Config) (Config, error) {
 	cfg, err := v1.ResolveConsts(cfg)
 	if err != nil {
-		return Config{}, errors.Wrap(err, "error resolving consts")
+		return Config{}, fmt.Errorf("resolving consts: %w", err)
 	}
 
 	var rules []Rule
diff --git a/pkg/config/v1alpha3/import.go b/pkg/config/v1alpha3/import.go
index df92fdc..a53d224 100644
--- a/pkg/config/v1alpha3/import.go
+++ b/pkg/config/v1alpha3/import.go
@@ -1,8 +1,9 @@
 package v1alpha3
 
 import (
+	"fmt"
+
 	"github.com/hashicorp/go-multierror"
-	"github.com/pkg/errors"
 
 	v2 "github.com/mbrt/gmailctl/pkg/config/v1alpha2"
 )
@@ -29,7 +30,7 @@ func (i *importer) Import(cfg v2.Config) (Config, error) {
 		rules = append(rules, i.importRule(r))
 		if err := i.resetError(); err != nil {
 			finalErr = multierror.Append(finalErr,
-				errors.Wrapf(err, "invalid rule: %s", r),
+				fmt.Errorf("in rule %s: %w", r, err),
 			)
 		}
 	}
@@ -49,7 +50,7 @@ func (i *importer) importNamedFilters(fs []v2.NamedFilter) {
 		i.nmap[f.Name] = i.importFilter(f.Query)
 		if err := i.resetError(); err != nil {
 			finalErr = multierror.Append(finalErr,
-				errors.Wrapf(err, "invalid filter '%s': %s", f.Name, f.Query),
+				fmt.Errorf("in filter '%s' %s: %w", f.Name, f.Query, err),
 			)
 		}
 	}
@@ -110,7 +111,7 @@ func (i *importer) importRefName(name string) FilterNode {
 		return n
 	}
 	i.err = multierror.Append(i.err,
-		errors.Errorf("filter name '%s' not found", name))
+		fmt.Errorf("filter name '%s' not found", name))
 	return dummyFilter
 }
 
diff --git a/pkg/errors/err.go b/pkg/errors/err.go
new file mode 100644
index 0000000..a879413
--- /dev/null
+++ b/pkg/errors/err.go
@@ -0,0 +1,80 @@
+package errors
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"strings"
+)
+
+// Aliases to the standard errors package.
+var (
+	New = errors.New
+	Is  = errors.Is
+	As  = errors.As
+)
+
+// New is an alias for errors.New.
+
+// WithCause annotates a symptom error with a cause.
+//
+// Both errors can be discovered by the Is and As methods.
+func WithCause(symptom, cause error) error {
+	return annotated{
+		cause:   cause,
+		symptom: symptom,
+	}
+}
+
+func WithDetails(err error, details ...string) error {
+	if err == nil {
+		return nil
+	}
+	return detailed{err, details}
+}
+
+func Details(err error) string {
+	var (
+		buffer bytes.Buffer
+		dErr   detailed
+	)
+	for errors.As(err, &dErr) {
+		// Append all details of this error.
+		for _, d := range dErr.details {
+			buffer.WriteString("\n  - ")
+			buffer.WriteString(strings.ReplaceAll(d, "\n", "\n    "))
+		}
+		// Continue down the chain.
+		err = dErr.error
+	}
+	return buffer.String()
+}
+
+type detailed struct {
+	error
+	details []string
+}
+
+type annotated struct {
+	cause   error
+	symptom error
+}
+
+func (e annotated) Error() string {
+	return fmt.Sprintf("%s: %s", e.cause, e.symptom)
+}
+
+func (e annotated) Unwrap() error {
+	return e.cause
+}
+
+func (e annotated) Is(target error) bool {
+	return errors.Is(e.symptom, target) || errors.Is(e.cause, target)
+}
+
+func (e annotated) As(target interface{}) bool {
+	if errors.As(e.symptom, target) {
+		return true
+	}
+	return errors.As(e.cause, target)
+}
diff --git a/pkg/export/api/api_export.go b/pkg/export/api/api_export.go
index 1bb8c3f..3b0b040 100644
--- a/pkg/export/api/api_export.go
+++ b/pkg/export/api/api_export.go
@@ -1,9 +1,9 @@
 package api
 
 import (
+	"errors"
 	"fmt"
 
-	"github.com/pkg/errors"
 	gmailv1 "google.golang.org/api/gmail/v1"
 
 	"github.com/mbrt/gmailctl/pkg/filter"
@@ -16,7 +16,7 @@ func Export(filters filter.Filters, lmap LabelMap) ([]*gmailv1.Filter, error) {
 	for i, filter := range filters {
 		ef, err := export(filter, lmap)
 		if err != nil {
-			return nil, errors.Wrap(err, fmt.Sprintf("error exporting filter #%d", i))
+			return nil, fmt.Errorf("exporting filter #%d: %w", i, err)
 		}
 		res[i] = ef
 	}
@@ -33,11 +33,11 @@ func export(filter filter.Filter, lmap LabelMap) (*gmailv1.Filter, error) {
 
 	action, err := exportAction(filter.Action, lmap)
 	if err != nil {
-		return nil, errors.Wrap(err, "error in export action")
+		return nil, fmt.Errorf("in export action: %w", err)
 	}
 	criteria, err := exportCriteria(filter.Criteria)
 	if err != nil {
-		return nil, errors.Wrap(err, "error in export criteria")
+		return nil, fmt.Errorf("in export criteria: %w", err)
 	}
 
 	return &gmailv1.Filter{
@@ -60,7 +60,7 @@ func exportAction(action filter.Actions, lmap LabelMap) (*gmailv1.FilterAction,
 	if action.AddLabel != "" {
 		id, ok := lmap.NameToID(action.AddLabel)
 		if !ok {
-			return nil, errors.Errorf("label '%s' not found", action.AddLabel)
+			return nil, fmt.Errorf("label %q not found", action.AddLabel)
 		}
 		lops.AddLabel(id)
 	}
@@ -109,7 +109,7 @@ func exportCategory(category gmail.Category) (string, error) {
 	case gmail.CategoryPromotions:
 		return labelIDCategoryPromotions, nil
 	}
-	return "", errors.Errorf("unknown category '%s'", category)
+	return "", fmt.Errorf("unknown category %q", category)
 }
 
 func exportCriteria(criteria filter.Criteria) (*gmailv1.FilterCriteria, error) {
diff --git a/pkg/export/api/api_import.go b/pkg/export/api/api_import.go
index bb9cf9e..644c6d2 100644
--- a/pkg/export/api/api_import.go
+++ b/pkg/export/api/api_import.go
@@ -1,9 +1,10 @@
 package api
 
 import (
+	"errors"
 	"fmt"
+
 	"github.com/hashicorp/go-multierror"
-	"github.com/pkg/errors"
 	gmailv1 "google.golang.org/api/gmail/v1"
 
 	"github.com/mbrt/gmailctl/pkg/filter"
@@ -22,7 +23,7 @@ func Import(filters []*gmailv1.Filter, lmap LabelMap) (filter.Filters, error) {
 		impFilter, err := importFilter(gfilter, lmap)
 		if err != nil {
 			// We don't want to return here, but continue and skip the problematic filter
-			err = errors.Wrap(err, fmt.Sprintf("error importing filter '%s'", gfilter.Id))
+			err = fmt.Errorf("importing filter %q: %w", gfilter.Id, err)
 			reserr = multierror.Append(reserr, err)
 		} else {
 			res = append(res, impFilter)
@@ -35,11 +36,11 @@ func Import(filters []*gmailv1.Filter, lmap LabelMap) (filter.Filters, error) {
 func importFilter(gf *gmailv1.Filter, lmap LabelMap) (filter.Filter, error) {
 	action, err := importAction(gf.Action, lmap)
 	if err != nil {
-		return filter.Filter{}, errors.Wrap(err, "error importing action")
+		return filter.Filter{}, fmt.Errorf("importing action: %w", err)
 	}
 	criteria, err := importCriteria(gf.Criteria)
 	if err != nil {
-		return filter.Filter{}, errors.Wrap(err, "error importing criteria")
+		return filter.Filter{}, fmt.Errorf("importing criteria: %w", err)
 	}
 	return filter.Filter{
 		ID:       gf.Id,
@@ -72,7 +73,7 @@ func importAddLabels(res *filter.Actions, addLabelIDs []string, lmap LabelMap) e
 		category := importCategory(labelID)
 		if category != "" {
 			if res.Category != "" {
-				return errors.Errorf("multiple categories: '%s', '%s'", category, res.Category)
+				return fmt.Errorf("multiple categories: '%s', '%s'", category, res.Category)
 			}
 			res.Category = category
 			continue
@@ -89,7 +90,7 @@ func importAddLabels(res *filter.Actions, addLabelIDs []string, lmap LabelMap) e
 			// it should be a label to add
 			labelName, ok := lmap.IDToName(labelID)
 			if !ok {
-				return errors.Errorf("unknown label ID '%s'", labelID)
+				return fmt.Errorf("unknown label ID '%s'", labelID)
 			}
 			res.AddLabel = labelName
 		}
@@ -110,7 +111,7 @@ func importRemoveLabels(res *filter.Actions, removeLabelIDs []string) error {
 			res.MarkNotSpam = true
 		default:
 			// filters not added by us are not supported
-			return errors.Errorf("unupported label to remove '%s'", labelID)
+			return fmt.Errorf("unupported label to remove %q", labelID)
 		}
 	}
 	return nil
diff --git a/pkg/export/xml/consts.go b/pkg/export/xml/consts.go
index 21f3a7a..6d5483d 100644
--- a/pkg/export/xml/consts.go
+++ b/pkg/export/xml/consts.go
@@ -4,8 +4,6 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/pkg/errors"
-
 	"github.com/mbrt/gmailctl/pkg/gmail"
 )
 
@@ -51,7 +49,7 @@ func categoryToSmartLabel(cat gmail.Category) (string, error) {
 		smartl = SmartLabelPromo
 	default:
 		possib := gmail.PossibleCategoryValues()
-		return "", errors.Errorf("unrecognized category '%s' (possible values: %s)",
+		return "", fmt.Errorf("unrecognized category %q (possible values: %s)",
 			cat, strings.Join(possib, ", "))
 	}
 	return fmt.Sprintf("^smartlabel_%s", smartl), nil
diff --git a/pkg/filter/convert.go b/pkg/filter/convert.go
index 2615e45..593cae6 100644
--- a/pkg/filter/convert.go
+++ b/pkg/filter/convert.go
@@ -1,11 +1,10 @@
 package filter
 
 import (
+	"errors"
 	"fmt"
 	"strings"
 
-	"github.com/pkg/errors"
-
 	"github.com/mbrt/gmailctl/pkg/parser"
 )
 
@@ -15,7 +14,7 @@ func FromRules(rs []parser.Rule) (Filters, error) {
 	for i, rule := range rs {
 		filters, err := FromRule(rule)
 		if err != nil {
-			return res, errors.Wrap(err, fmt.Sprintf("error generating rule #%d", i))
+			return res, fmt.Errorf("generating rule #%d: %w", i, err)
 		}
 		res = append(res, filters...)
 	}
@@ -28,14 +27,14 @@ func FromRule(rule parser.Rule) ([]Filter, error) {
 	for _, c := range splitRootOr(rule.Criteria) {
 		criteria, err := GenerateCriteria(c)
 		if err != nil {
-			return nil, errors.Wrap(err, "error generating criteria")
+			return nil, fmt.Errorf("generating criteria: %w", err)
 		}
 		crits = append(crits, criteria)
 	}
 
 	actions, err := generateActions(rule.Actions)
 	if err != nil {
-		return nil, errors.Wrap(err, "error generating actions")
+		return nil, fmt.Errorf("generating actions: %w", err)
 	}
 
 	return combineCriteriaWithActions(crits, actions), nil
@@ -82,7 +81,7 @@ func generateNode(node *parser.Node) (Criteria, error) {
 
 	case parser.OperationNot:
 		if ln := len(node.Children); ln != 1 {
-			return Criteria{}, errors.Errorf("after 'not' got %d children, expected 1", ln)
+			return Criteria{}, fmt.Errorf("after 'not' got %d children, expected 1", ln)
 		}
 		cq, err := generateCriteriaAsString(node.Children[0])
 		return Criteria{
@@ -90,7 +89,7 @@ func generateNode(node *parser.Node) (Criteria, error) {
 		}, err
 	}
 
-	return Criteria{}, errors.Errorf("unknown node operation %d", node.Operation)
+	return Criteria{}, fmt.Errorf("unknown node operation %d", node.Operation)
 }
 
 func generateLeaf(leaf *parser.Leaf) (Criteria, error) {
@@ -133,7 +132,7 @@ func generateLeaf(leaf *parser.Leaf) (Criteria, error) {
 			Query: query,
 		}, nil
 	default:
-		return Criteria{}, errors.Errorf("unknown function type %d", leaf.Function)
+		return Criteria{}, fmt.Errorf("unknown function type %d", leaf.Function)
 	}
 }
 
@@ -188,7 +187,7 @@ func groupWithOperation(query string, op parser.OperationType) (string, error) {
 	case parser.OperationNot:
 		return fmt.Sprintf("-%s", query), nil
 	default:
-		return "", errors.Errorf("unknown node operation %d", op)
+		return "", fmt.Errorf("unknown node operation %d", op)
 	}
 }
 
diff --git a/pkg/label/diff.go b/pkg/label/diff.go
index 6181fad..22d8cf4 100644
--- a/pkg/label/diff.go
+++ b/pkg/label/diff.go
@@ -5,7 +5,6 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/pkg/errors"
 	"github.com/pmezard/go-difflib/difflib"
 
 	"github.com/mbrt/gmailctl/pkg/filter"
@@ -127,7 +126,7 @@ type ModifiedLabel struct {
 func Validate(d LabelsDiff, filters filter.Filters) error {
 	for _, l := range d.Removed {
 		if filters.HasLabel(l.Name) {
-			return errors.Errorf("cannot remove label '%s', used in filter", l.Name)
+			return fmt.Errorf("cannot remove label %q, used in filter", l.Name)
 		}
 	}
 	return nil
diff --git a/pkg/label/label.go b/pkg/label/label.go
index 30a56ee..b0f1d71 100644
--- a/pkg/label/label.go
+++ b/pkg/label/label.go
@@ -1,11 +1,10 @@
 package label
 
 import (
+	"errors"
 	"fmt"
 	"strings"
 
-	"github.com/pkg/errors"
-
 	cfgv3 "github.com/mbrt/gmailctl/pkg/config/v1alpha3"
 )
 
@@ -30,13 +29,13 @@ func (ls Labels) Validate() error {
 			return errors.New("invalid label without a name")
 		}
 		if strings.HasPrefix(n, "/") {
-			return errors.Errorf("label '%s' shouldn't start with /", n)
+			return fmt.Errorf("label %q shouldn't start with /", n)
 		}
 		if strings.HasSuffix(n, "/") {
-			return errors.Errorf("label '%s' shouldn't end with /", n)
+			return fmt.Errorf("label %q shouldn't end with /", n)
 		}
 		if _, ok := lmap[n]; ok {
-			return errors.Errorf("label '%s' provided multiple times", n)
+			return fmt.Errorf("label %q provided multiple times", n)
 		}
 		lmap[n] = struct{}{}
 	}
diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go
index 4ad67fe..a674bce 100644
--- a/pkg/parser/parser.go
+++ b/pkg/parser/parser.go
@@ -1,10 +1,10 @@
 package parser
 
 import (
+	"fmt"
+	"errors"
 	"strings"
 
-	"github.com/pkg/errors"
-
 	cfg "github.com/mbrt/gmailctl/pkg/config/v1alpha3"
 )
 
@@ -26,12 +26,12 @@ func Parse(config cfg.Config) ([]Rule, error) {
 	for i, rule := range config.Rules {
 		crit, err := parseCriteria(rule.Filter)
 		if err != nil {
-			return nil, errors.Wrapf(err, "error parsing criteria for rule #%d", i)
+			return nil, fmt.Errorf("parsing criteria for rule #%d: %w", i, err)
 		}
 
 		scrit, err := SimplifyCriteria(crit)
 		if err != nil {
-			return nil, errors.Wrapf(err, "error simplifying criteria for rule #%d", i)
+			return nil, fmt.Errorf("simplifying criteria for rule #%d: %w", i, err)
 		}
 
 		res = append(res, Rule{
@@ -82,7 +82,7 @@ func checkSyntax(f cfg.FilterNode) error {
 		if len(fs) == 0 {
 			return errors.New("empty filter node")
 		}
-		return errors.Errorf("multiple fields specified in the same filter node: %s",
+		return fmt.Errorf("multiple fields specified in the same filter node: %s",
 			strings.Join(fs, ","))
 	}
 	if !f.IsEscaped {
@@ -96,7 +96,7 @@ func checkSyntax(f cfg.FilterNode) error {
 			return nil
 		}
 	}
-	return errors.Errorf("'isRaw' can be used only with fields %s", strings.Join(allowed, ", "))
+	return fmt.Errorf("'isRaw' can be used only with fields %s", strings.Join(allowed, ", "))
 }
 
 func parseOperation(f cfg.FilterNode) (OperationType, []cfg.FilterNode) {
diff --git a/pkg/reporting/err.go b/pkg/reporting/pretty.go
similarity index 100%
rename from pkg/reporting/err.go
rename to pkg/reporting/pretty.go
diff --git a/pkg/rimport/rimport.go b/pkg/rimport/rimport.go
index c258b46..34fd275 100644
--- a/pkg/rimport/rimport.go
+++ b/pkg/rimport/rimport.go
@@ -1,10 +1,10 @@
 package rimport
 
 import (
+	"errors"
+	"fmt"
 	"strings"
 
-	"github.com/pkg/errors"
-
 	config "github.com/mbrt/gmailctl/pkg/config/v1alpha3"
 	"github.com/mbrt/gmailctl/pkg/filter"
 	"github.com/mbrt/gmailctl/pkg/label"
@@ -17,7 +17,7 @@ func Import(fs filter.Filters, ls label.Labels) (config.Config, error) {
 	for i, f := range fs {
 		r, err := fromFilter(f)
 		if err != nil {
-			return config.Config{}, errors.Wrapf(err, "error importing filter #%d", i)
+			return config.Config{}, fmt.Errorf("importing filter #%d: %w", i, err)
 		}
 		rules = append(rules, r)
 	}
@@ -131,7 +131,7 @@ func fromActions(c filter.Actions) (config.Actions, error) {
 	var err error
 	res.MarkImportant, err = handleTribool(c.MarkImportant, c.MarkNotImportant)
 	if err != nil {
-		return res, errors.Wrap(err, "error in 'mark important'")
+		return res, fmt.Errorf("in 'mark important': %w", err)
 	}
 	if c.MarkNotSpam {
 		res.MarkSpam = boolPtr(false)


Test patch:
diff --git a/pkg/cfgtest/cfgtest.go b/pkg/cfgtest/cfgtest.go
index 958cd0c..911ca6f 100644
--- a/pkg/cfgtest/cfgtest.go
+++ b/pkg/cfgtest/cfgtest.go
@@ -4,7 +4,10 @@ import (
 	"fmt"
 	"sort"
 
+	"github.com/pmezard/go-difflib/difflib"
+
 	cfg "github.com/mbrt/gmailctl/pkg/config/v1alpha3"
+	"github.com/mbrt/gmailctl/pkg/errors"
 	"github.com/mbrt/gmailctl/pkg/gmail"
 	"github.com/mbrt/gmailctl/pkg/parser"
 	"github.com/mbrt/gmailctl/pkg/reporting"
@@ -51,7 +54,7 @@ func (rs Rules) ExecTests(ts []cfg.Test) error {
 			if name == "" {
 				name = fmt.Sprintf("#%d", i)
 			}
-			return fmt.Errorf("test '%s' failed: %w", name, err)
+			return fmt.Errorf("test %q: %w", name, err)
 		}
 	}
 	return nil
@@ -64,11 +67,33 @@ func (rs Rules) ExecTest(t cfg.Test) error {
 	for i, msg := range t.Messages {
 		expected, err := rs.MatchingActions(msg)
 		if err != nil {
-			return fmt.Errorf("message #%d: error evaluating matching filters: %w", i, err)
+			return errors.WithDetails(
+				fmt.Errorf("message #%d: error evaluating matching filters: %w", i, err),
+				messageDetails(msg))
+		}
+		if expected.Equal(Actions(t.Actions)) {
+			// All good with this message.
+			continue
 		}
-		if !expected.Equal(Actions(t.Actions)) {
-			return fmt.Errorf("message #%d is going to get unexpected actions: %s", i, reporting.Prettify(expected, true))
+
+		// Report the error.
+		diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
+			A:        difflib.SplitLines(reporting.Prettify(expected, false)),
+			B:        difflib.SplitLines(reporting.Prettify(t.Actions, false)),
+			FromFile: "want",
+			ToFile:   "got",
+			Context:  5,
+		})
+		if err != nil {
+			// The diff failing is not a big deal, but we should return
+			// something.
+			diff = fmt.Sprintf("<cannot compute diff>: %v", err)
 		}
+		return errors.WithDetails(
+			fmt.Errorf("message #%d is going to get unexpected actions: %s", i,
+				reporting.Prettify(expected, true)),
+			messageDetails(msg),
+			fmt.Sprintf("Actions:\n%s", diff))
 	}
 	return nil
 }
@@ -209,3 +234,7 @@ func stringSliceEqual(s1, s2 []string) bool {
 	}
 	return true
 }
+
+func messageDetails(msg cfg.Message) string {
+	return fmt.Sprintf("Message: %s", reporting.Prettify(msg, false))
+}
diff --git a/pkg/errors/err_test.go b/pkg/errors/err_test.go
new file mode 100644
index 0000000..93fd379
--- /dev/null
+++ b/pkg/errors/err_test.go
@@ -0,0 +1,61 @@
+package errors
+
+import (
+	"errors"
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+type errType struct {
+	a int
+}
+
+func (errType) Error() string { return "errType" }
+
+func TestAnnotated(t *testing.T) {
+	err1 := errors.New("error1")
+	err2 := errType{3}
+	wrapped := WithCause(err2, err1)
+
+	// The error is both err1 and err2
+	assert.Equal(t, "error1: errType", wrapped.Error())
+	assert.True(t, errors.Is(wrapped, err1))
+	assert.True(t, errors.Is(wrapped, err2))
+
+	// Contents are preserved.
+	var et errType
+	assert.True(t, errors.As(wrapped, &et))
+	assert.Equal(t, err2, et)
+
+	// Same properties with the errors inverted.
+	wrapped = WithCause(err1, err2)
+	assert.True(t, errors.Is(wrapped, err1))
+	assert.True(t, errors.Is(wrapped, err2))
+	assert.True(t, errors.As(wrapped, &et))
+	assert.Equal(t, err2, et)
+}
+
+func TestErrWithDetails(t *testing.T) {
+	err1 := errors.New("err1")
+	err2 := WithDetails(
+		fmt.Errorf("err2: %w", err1),
+		"second descr\nmultiline",
+		"another\ndescr",
+	)
+	err3 := WithDetails(
+		fmt.Errorf("err3: %w", err2),
+		"third descr\nmultiline\nmultiline again")
+	err4 := fmt.Errorf("err4: %w", err3)
+	assert.Equal(t, "err4: err3: err2: err1", fmt.Sprintf("%v", err4))
+	details := `
+  - third descr
+    multiline
+    multiline again
+  - second descr
+    multiline
+  - another
+    descr`
+	assert.Equal(t, details, Details(err4))
+}