├── .github └── workflows │ └── go.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── _examples ├── basic │ ├── main.go │ └── nestedpackage │ │ └── nested.go └── helpers │ ├── built-in │ └── main.go │ └── custom │ └── main.go ├── benchmarks_test.go ├── chain.go ├── errors.go ├── errors_go1.20.go ├── errors_go1.20_test.go ├── errors_test.go ├── go.mod ├── go.sum ├── helpers.go └── helpers ├── awserrors └── awserrors.go ├── ioerrors └── ioerrors.go └── neterrors └── neterrors.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | name: Test 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | go-version: [1.18.x, 1.21.0] 12 | os: [ubuntu-latest, macos-latest, windows-latest] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - name: Install Go 16 | uses: actions/setup-go@v3 17 | with: 18 | go-version: ${{ matrix.go-version }} 19 | 20 | - name: Checkout code 21 | uses: actions/checkout@v3 22 | 23 | - name: Restore Cache 24 | uses: actions/cache@v3 25 | with: 26 | path: ~/go/pkg/mod 27 | key: ${{ runner.os }}-v1-go-${{ hashFiles('**/go.sum') }} 28 | restore-keys: | 29 | ${{ runner.os }}-v1-go- 30 | 31 | - name: Test 32 | run: go test -race ./... 33 | 34 | golangci: 35 | name: lint 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/setup-go@v3 39 | with: 40 | go-version: 1.21.x 41 | - uses: actions/checkout@v3 42 | - name: golangci-lint 43 | uses: golangci/golangci-lint-action@v3 44 | with: 45 | version: latest 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [5.4.0] - 2023-10-18 10 | ### Added 11 | - Join function to join multiple errors into a single error to continue to be a drop-in replacement to the std library. 12 | 13 | ## [5.3.3] - 2023-08-16 14 | ### Fixed 15 | - First error inconsistently wrapped with error and prefix instead of err then prefix in the chain. 16 | - Corrected Is & As functions to directly causal error. 17 | 18 | ## [5.3.2] - 2023-08-16 19 | ### Fixed 20 | - Link Error output missing error when prefix was blank. 21 | 22 | ## [5.3.1] - 2023-08-15 23 | ### Fixed 24 | - Wrap recursively wrapping the Chain itself instead of only adding another Link. 25 | 26 | ## [5.3.0] - 2023-04-14 27 | ### Fixed 28 | - Added Error interface for Link. 29 | 30 | ## [5.2.3] - 2022-05-30 31 | ### Fixed 32 | - Fixed errors.As wrapper, linter is wrong. 33 | 34 | ## [5.2.2] - 2022-05-30 35 | ### Fixed 36 | - Fixed calling error helpers on first wrap only, where the source of the data will be. 37 | 38 | ## [5.2.1] - 2022-05-30 39 | ### Fixed 40 | - Changelog pointing to wrong repo. 41 | 42 | ## [5.2.0] - 2022-05-28 43 | ### Added 44 | - Wrap check for nil errors which will now panic to indicate a larger issue, the caller NOT checking an error value. This caused a panic when trying to print the error if nil error was wrapped. 45 | 46 | ### Removed 47 | - Deprecated information in the documentation. 48 | 49 | ### Changed 50 | - Updated deps. 51 | 52 | 53 | [Unreleased]: https://github.com/go-playground/errors/compare/v5.4.0...HEAD 54 | [5.4.0]: https://github.com/go-playground/errors/compare/v5.3.3...v5.4.0 55 | [5.3.3]: https://github.com/go-playground/errors/compare/v5.3.2...v5.3.3 56 | [5.3.2]: https://github.com/go-playground/errors/compare/v5.3.1...v5.3.2 57 | [5.3.1]: https://github.com/go-playground/errors/compare/v5.3.0...v5.3.1 58 | [5.3.0]: https://github.com/go-playground/errors/compare/v5.2.3...v5.3.0 59 | [5.2.3]: https://github.com/go-playground/errors/compare/v5.2.2...v5.2.3 60 | [5.2.2]: https://github.com/go-playground/errors/compare/v5.2.1...v5.2.2 61 | [5.2.1]: https://github.com/go-playground/errors/compare/v5.2.0...v5.2.1 62 | [5.2.0]: https://github.com/go-playground/errors/compare/v5.1.1...v5.2.0 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Dean Karn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOCMD=GO111MODULE=on go 2 | 3 | linters-install: 4 | @golangci-lint --version >/dev/null 2>&1 || { \ 5 | echo "installing linting tools..."; \ 6 | brew install golangci-lint; \ 7 | } 8 | 9 | lint: linters-install 10 | @golangci-lint run 11 | 12 | test: 13 | $(GOCMD) test -cover -race ./... 14 | 15 | bench: 16 | $(GOCMD) test -run=NONE -bench=. -benchmem ./... 17 | 18 | .PHONY: test lint linters-install -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Package errors 2 | ============ 3 | ![Project status](https://img.shields.io/badge/version-5.4.0-green.svg) 4 | [![Build Status](https://travis-ci.org/go-playground/errors.svg?branch=master)](https://travis-ci.org/go-playground/errors) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/go-playground/errors)](https://goreportcard.com/report/github.com/go-playground/errors) 6 | [![GoDoc](https://godoc.org/github.com/go-playground/errors?status.svg)](https://pkg.go.dev/github.com/go-playground/errors/v5) 7 | ![License](https://img.shields.io/dub/l/vibe-d.svg) 8 | 9 | Package errors is an errors wrapping package to help propagate and chain errors as well as attach 10 | stack traces, tags(additional information) and even a Type classification system to categorize errors into types eg. Permanent vs Transient. 11 | 12 | It is a drop in replacement for the std Go [errors](https://golang.org/pkg/errors/) package. 13 | It is also 100% compatible with the Is, As and Unwrap interfaces used within the std library and can be mixed with the built-in error wrapping. 14 | 15 | Common Questions 16 | 17 | Why another package? 18 | There are two main reasons. 19 | - I think that the programs generating the original error(s) should be responsible for adding internal metadata and details to them. Ultimately developers are concerned with: 20 | - If the error is Transient or Permanent for retries. 21 | - Additional details for logging. 22 | 23 | - IMO most of the existing packages either don't take the error handling far enough, too far or down right unfriendly to use/consume. 24 | 25 | Features 26 | -------- 27 | - [x] works with go-playground/log, the Tags will be added as Field Key Values and Types will be concatenated as well when using `WithError` 28 | - [x] helpers to extract and classify error types using `RegisterHelper(...)`, many already existing such as ioerrors, neterrors, awserrors... 29 | - [x] built in helpers only need to be imported, eg. `_ github.com/go-playground/errors/v5/helpers/neterrors` allowing libraries to register their own helpers not needing the caller to do or guess what needs to be imported. 30 | 31 | Installation 32 | ------------ 33 | 34 | Use go get. 35 | 36 | go get -u github.com/go-playground/errors/v5 37 | 38 | Usage 39 | ----- 40 | ```go 41 | package main 42 | 43 | import ( 44 | "fmt" 45 | "io" 46 | 47 | "github.com/go-playground/errors/v5" 48 | ) 49 | 50 | func main() { 51 | err := level1("testing error") 52 | fmt.Println(err) 53 | if errors.HasType(err, "Permanent") { 54 | // os.Exit(1) 55 | fmt.Println("it is a permanent error") 56 | } 57 | 58 | // root error 59 | cause := errors.Cause(err) 60 | fmt.Println("CAUSE:", cause) 61 | 62 | // can even still inspect the internal error 63 | fmt.Println(errors.Cause(err) == io.EOF) // will extract the cause for you 64 | fmt.Println(errors.Cause(cause) == io.EOF) 65 | 66 | // and still in a switch 67 | switch errors.Cause(err) { 68 | case io.EOF: 69 | fmt.Println("EOF error") 70 | default: 71 | fmt.Println("unknown error") 72 | } 73 | } 74 | 75 | func level1(value string) error { 76 | if err := level2(value); err != nil { 77 | return errors.Wrap(err, "level2 call failed") 78 | } 79 | return nil 80 | } 81 | 82 | func level2(value string) error { 83 | err := io.EOF 84 | return errors.Wrap(err, "failed to do something").AddTypes("Permanent").AddTags(errors.T("value", value)) 85 | } 86 | ``` 87 | 88 | Package Versioning 89 | ---------- 90 | Using Go modules and proper semantic version releases (as always). 91 | 92 | License 93 | ------ 94 | Distributed under MIT License, please see license file in code for more details. 95 | -------------------------------------------------------------------------------- /_examples/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/go-playground/errors/v5" 8 | nestedpackagee "github.com/go-playground/errors/v5/_examples/basic/nestedpackage" 9 | ) 10 | 11 | func main() { 12 | err := level1Function("1") 13 | fmt.Println(err) 14 | if errors.HasType(err, "Permanent") { 15 | // os.Exit(1) 16 | fmt.Println("it is a permanent error") 17 | } 18 | 19 | // root error 20 | cause := errors.Cause(err) 21 | fmt.Println("CAUSE:", cause) 22 | 23 | // can even still inspect the internal error 24 | fmt.Println(errors.Cause(err) == io.EOF) // will extract the cause for you 25 | fmt.Println(errors.Cause(cause) == io.EOF) 26 | 27 | // and still in a switch 28 | switch errors.Cause(err) { 29 | case io.EOF: 30 | fmt.Println("EOF error") 31 | default: 32 | fmt.Println("unknown error") 33 | } 34 | } 35 | 36 | func level1Function(userID string) error { 37 | if err := level2Function(userID); err != nil { 38 | return errors.Wrap(err, "level2 call failed") 39 | } 40 | return nil 41 | } 42 | 43 | func level2Function(userID string) error { 44 | err := nestedpackagee.GetUser(userID) 45 | return errors.Wrap(err, "GetUser something went wrong") 46 | } 47 | -------------------------------------------------------------------------------- /_examples/basic/nestedpackage/nested.go: -------------------------------------------------------------------------------- 1 | package nestedpackagee 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/go-playground/errors/v5" 7 | ) 8 | 9 | func GetUser(userID string) error { 10 | err := io.EOF 11 | return errors.Wrap(err, "failed to do something").AddTypes("Permanent").AddTags(errors.T("user_id", userID)) 12 | } 13 | -------------------------------------------------------------------------------- /_examples/helpers/built-in/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/go-playground/errors/v5" 8 | // init function handles registration automatically 9 | _ "github.com/go-playground/errors/v5/helpers/neterrors" 10 | ) 11 | 12 | func main() { 13 | _, err := net.ResolveIPAddr("tcp", "foo") 14 | if err != nil { 15 | err = errors.Wrap(err, "failed to perform operation") 16 | } 17 | 18 | // all that extra context, types and tags captured for free 19 | // there are more helpers and you can even create your own. 20 | fmt.Println(err) 21 | } 22 | -------------------------------------------------------------------------------- /_examples/helpers/custom/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/go-playground/errors/v5" 8 | ) 9 | 10 | func main() { 11 | errors.RegisterHelper(MyCustomErrHandler) 12 | _, err := net.ResolveIPAddr("tcp", "foo") 13 | if err != nil { 14 | err = errors.Wrap(err, "failed to perform operation") 15 | } 16 | 17 | // all that extra context, types and tags captured for free 18 | // there are more helpers and you can even create your own. 19 | fmt.Println(err) 20 | } 21 | 22 | // MyCustomErrHandler helps classify my errors 23 | func MyCustomErrHandler(c errors.Chain, err error) (cont bool) { 24 | switch err.(type) { 25 | case net.UnknownNetworkError: 26 | _ = c.AddTypes("io").AddTag("additional", "details") 27 | return 28 | //case net.Other: 29 | // ... 30 | // return 31 | } 32 | return true 33 | } 34 | -------------------------------------------------------------------------------- /benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func BenchmarkErrorNew(b *testing.B) { 8 | for i := 0; i < b.N; i++ { 9 | _ = New("base error") 10 | } 11 | } 12 | 13 | func BenchmarkErrorWrap(b *testing.B) { 14 | err := New("base error") 15 | for i := 0; i < b.N; i++ { 16 | _ = Wrap(err, "wrapped error") 17 | } 18 | } 19 | 20 | func BenchmarkErrorParallelNew(b *testing.B) { 21 | b.RunParallel(func(pb *testing.PB) { 22 | for pb.Next() { 23 | _ = New("base error") 24 | } 25 | }) 26 | } 27 | 28 | func BenchmarkErrorPrint(b *testing.B) { 29 | err := New("base error") 30 | for i := 0; i < b.N; i++ { 31 | _ = err.Error() 32 | } 33 | } 34 | 35 | func BenchmarkErrorParallelPrint(b *testing.B) { 36 | err := New("base error") 37 | b.RunParallel(func(pb *testing.PB) { 38 | for pb.Next() { 39 | _ = err.Error() 40 | } 41 | }) 42 | } 43 | 44 | func BenchmarkErrorPrintWithTagsAndTypes(b *testing.B) { 45 | err := New("base error").AddTag("key", "value").AddTypes("Permanent", "Other") 46 | for i := 0; i < b.N; i++ { 47 | _ = err.Error() 48 | } 49 | } 50 | 51 | func BenchmarkErrorParallelPrintWithTagsAndTypes(b *testing.B) { 52 | err := New("base error").AddTag("key", "value").AddTypes("Permanent", "Other") 53 | b.RunParallel(func(pb *testing.PB) { 54 | for pb.Next() { 55 | _ = err.Error() 56 | } 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /chain.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | stderrors "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | runtimeext "github.com/go-playground/pkg/v5/runtime" 10 | unsafeext "github.com/go-playground/pkg/v5/unsafe" 11 | ) 12 | 13 | var ( 14 | _ unwrap = (*Chain)(nil) 15 | _ is = (*Chain)(nil) 16 | _ as = (*Chain)(nil) 17 | ) 18 | 19 | // T is a shortcut to make a Tag 20 | func T(key string, value any) Tag { 21 | return Tag{Key: key, Value: value} 22 | } 23 | 24 | // Tag contains a single key value combination 25 | // to be attached to your error 26 | type Tag struct { 27 | Key string 28 | Value any 29 | } 30 | 31 | func newLink(err error, prefix string, skipFrames int) *Link { 32 | return &Link{ 33 | Err: err, 34 | Prefix: prefix, 35 | Source: runtimeext.StackLevel(skipFrames), 36 | } 37 | } 38 | 39 | // Chain contains the chained errors, the links, of the chains if you will 40 | type Chain []*Link 41 | 42 | // Error returns the formatted error string 43 | func (c Chain) Error() string { 44 | return errFormatFn(c) 45 | } 46 | 47 | // Link contains a single error entry contained in an error Chain. 48 | type Link struct { 49 | 50 | // Err is the wrapped error, either the original or already wrapped 51 | Err error 52 | 53 | // Prefix contains the error prefix text 54 | Prefix string 55 | 56 | // Type stores one or more categorized types of error set by the caller using AddTypes and is optional 57 | Types []string 58 | 59 | // Tags contains an array of tags associated with this error, if any 60 | Tags []Tag 61 | 62 | // Source contains the name, file and lines obtained from the stack trace 63 | Source runtimeext.Frame 64 | } 65 | 66 | // Error prints out a single Link in the Chains error. 67 | func (l *Link) Error() string { 68 | return unsafeext.BytesToString(l.formatError(make([]byte, 0, 64))) 69 | } 70 | 71 | // formatError prints a single Links error 72 | func (l *Link) formatError(b []byte) []byte { 73 | var funcName string 74 | 75 | b = append(b, "source="...) 76 | idx := strings.LastIndexByte(l.Source.Frame.Function, '.') 77 | if idx == -1 { 78 | b = append(b, l.Source.File()...) 79 | } else { 80 | funcName = l.Source.Frame.Function[idx+1:] 81 | remaining := l.Source.Frame.Function[:idx] 82 | 83 | idx = strings.LastIndexByte(remaining, '/') 84 | if idx > -1 { 85 | b = append(b, l.Source.Frame.Function[:idx+1]...) 86 | remaining = l.Source.Frame.Function[idx+1:] 87 | } 88 | 89 | idx = strings.IndexByte(remaining, '.') 90 | if idx == -1 { 91 | b = append(b, remaining...) 92 | } else { 93 | b = append(b, remaining[:idx]...) 94 | } 95 | b = append(b, '/') 96 | b = append(b, l.Source.File()...) 97 | } 98 | b = append(b, ':') 99 | b = strconv.AppendInt(b, int64(l.Source.Line()), 10) 100 | if funcName != "" { 101 | b = append(b, ':') 102 | b = append(b, funcName...) 103 | } 104 | b = append(b, ' ') 105 | b = append(b, "error="...) 106 | 107 | if l.Prefix != "" { 108 | b = append(b, l.Prefix...) 109 | } 110 | 111 | if l.Err != nil { 112 | if l.Prefix != "" { 113 | b = append(b, ": "...) 114 | } 115 | b = append(b, l.Err.Error()...) 116 | } 117 | 118 | for _, tag := range l.Tags { 119 | b = append(b, ' ') 120 | b = append(b, tag.Key...) 121 | b = append(b, '=') 122 | switch t := tag.Value.(type) { 123 | case string: 124 | b = append(b, t...) 125 | case int: 126 | b = strconv.AppendInt(b, int64(t), 10) 127 | case int8: 128 | b = strconv.AppendInt(b, int64(t), 10) 129 | case int16: 130 | b = strconv.AppendInt(b, int64(t), 10) 131 | case int32: 132 | b = strconv.AppendInt(b, int64(t), 10) 133 | case int64: 134 | b = strconv.AppendInt(b, t, 10) 135 | case uint: 136 | b = strconv.AppendUint(b, uint64(t), 10) 137 | case uint8: 138 | b = strconv.AppendUint(b, uint64(t), 10) 139 | case uint16: 140 | b = strconv.AppendUint(b, uint64(t), 10) 141 | case uint32: 142 | b = strconv.AppendUint(b, uint64(t), 10) 143 | case uint64: 144 | b = strconv.AppendUint(b, t, 10) 145 | case float32: 146 | b = strconv.AppendFloat(b, float64(t), 'g', -1, 32) 147 | case float64: 148 | b = strconv.AppendFloat(b, t, 'g', -1, 64) 149 | case bool: 150 | b = strconv.AppendBool(b, t) 151 | default: 152 | b = append(b, fmt.Sprintf("%v", tag.Value)...) 153 | } 154 | } 155 | 156 | if len(l.Types) > 0 { 157 | b = append(b, " types="...) 158 | for _, t := range l.Types { 159 | b = append(b, t...) 160 | b = append(b, ',') 161 | } 162 | b = b[:len(b)-1] 163 | } 164 | return b 165 | } 166 | 167 | // helper method to get the current *Link from the top level 168 | func (c Chain) current() *Link { 169 | return c[len(c)-1] 170 | } 171 | 172 | // AddTags allows the addition of multiple tags 173 | func (c Chain) AddTags(tags ...Tag) Chain { 174 | l := c.current() 175 | if len(l.Tags) == 0 { 176 | l.Tags = make([]Tag, 0, len(tags)) 177 | } 178 | l.Tags = append(l.Tags, tags...) 179 | return c 180 | } 181 | 182 | // AddTag allows the addition of a single tag 183 | func (c Chain) AddTag(key string, value any) Chain { 184 | return c.AddTags(Tag{Key: key, Value: value}) 185 | } 186 | 187 | // AddTypes sets one or more categorized types on the Link error 188 | func (c Chain) AddTypes(typ ...string) Chain { 189 | l := c.current() 190 | l.Types = append(l.Types, typ...) 191 | return c 192 | } 193 | 194 | // Wrap adds another contextual prefix to the error chain 195 | func (c Chain) Wrap(prefix string) Chain { 196 | return wrap(c, prefix, 3) 197 | } 198 | 199 | // Unwrap returns the result of calling the Unwrap method on an error, if the errors 200 | // type contains an Unwrap method returning error. 201 | // Otherwise, Unwrap returns nil. 202 | // 203 | // If attempting to retrieve the cause see Cause function instead. 204 | func (c Chain) Unwrap() error { 205 | if len(c) == 1 { 206 | if e, ok := c[0].Err.(unwrap); ok { 207 | return e.Unwrap() 208 | } 209 | return c[0].Err 210 | } 211 | return c[:len(c)-1] 212 | } 213 | 214 | // Is reports whether any error in error chain matches target. 215 | func (c Chain) Is(target error) bool { 216 | if len(c) == 0 { 217 | return false 218 | } 219 | if innerErr, ok := target.(Chain); ok { 220 | if len(innerErr) == 0 { 221 | return false 222 | } 223 | target = innerErr[0].Err 224 | } 225 | return stderrors.Is(c[0].Err, target) 226 | } 227 | 228 | // As finds the first error in the error chain that matches target, and if so, sets 229 | // target to that error value and returns true. Otherwise, it returns false. 230 | // 231 | // The chain consists of err itself followed by the sequence of errors obtained by 232 | // repeatedly calling Unwrap. 233 | // 234 | // An error matches target if the error's concrete value is assignable to the value 235 | // pointed to by target, or if the error has a method As(any) bool such that 236 | // As(target) returns true. In the latter case, the As method is responsible for 237 | // setting target. 238 | // 239 | // An error type might provide an As method, so it can be treated as if it were a 240 | // different error type. 241 | // 242 | // As panics if target is not a non-nil pointer to either a type that implements 243 | // error, or to any interface type. 244 | func (c Chain) As(target any) bool { 245 | if len(c) == 0 { 246 | return false 247 | } 248 | return stderrors.As(c[0].Err, target) 249 | } 250 | 251 | func defaultFormatFn(c Chain) string { 252 | b := make([]byte, 0, len(c)*192) 253 | 254 | for i := 0; i < len(c); i++ { 255 | b = c[i].formatError(b) 256 | b = append(b, '\n') 257 | } 258 | return unsafeext.BytesToString(b[:len(b)-1]) 259 | } 260 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | stderrors "errors" 5 | "fmt" 6 | "reflect" 7 | ) 8 | 9 | type unwrap interface{ Unwrap() error } 10 | type is interface{ Is(error) bool } 11 | type as interface{ As(any) bool } 12 | 13 | // ErrorFormatFn represents the error formatting function for a Chain of errors. 14 | type ErrorFormatFn func(Chain) string 15 | 16 | var ( 17 | helpers []Helper 18 | errFormatFn ErrorFormatFn = defaultFormatFn 19 | ) 20 | 21 | // RegisterHelper adds a new helper function to extract Type and Tag information. 22 | // errors will run all registered helpers until a match is found. 23 | // NOTE helpers are run in the order they are added. 24 | func RegisterHelper(helper Helper) { 25 | for i := 0; i < len(helpers); i++ { 26 | if reflect.ValueOf(helpers[i]).Pointer() == reflect.ValueOf(helper).Pointer() { 27 | return 28 | } 29 | } 30 | helpers = append(helpers, helper) 31 | } 32 | 33 | // RegisterErrorFormatFn sets a custom error formatting function in order for the error output to be customizable. 34 | func RegisterErrorFormatFn(fn ErrorFormatFn) { 35 | errFormatFn = fn 36 | } 37 | 38 | // New creates an error with the provided text and automatically wraps it with line information. 39 | func New(s string) Chain { 40 | return wrap(stderrors.New(s), "", 3) 41 | } 42 | 43 | // Newf creates an error with the provided text and automatically wraps it with line information. 44 | // it also accepts a variadic for optional message formatting. 45 | func Newf(format string, a ...any) Chain { 46 | return wrap(fmt.Errorf(format, a...), "", 3) 47 | } 48 | 49 | // Wrap encapsulates the error, stores a contextual prefix and automatically obtains 50 | // a stack trace. 51 | func Wrap(err error, prefix string) Chain { 52 | return wrap(err, prefix, 3) 53 | } 54 | 55 | // Wrapf encapsulates the error, stores a contextual prefix and automatically obtains 56 | // a stack trace. 57 | // it also accepts a variadic for prefix formatting. 58 | func Wrapf(err error, prefix string, a ...any) Chain { 59 | return wrap(err, fmt.Sprintf(prefix, a...), 3) 60 | } 61 | 62 | // WrapSkipFrames is a special version of Wrap that skips extra n frames when determining error location. 63 | // Normally only used when wrapping the library 64 | func WrapSkipFrames(err error, prefix string, n uint) Chain { 65 | return wrap(err, prefix, int(n)+3) 66 | } 67 | 68 | func wrap(err error, prefix string, skipFrames int) (c Chain) { 69 | if err == nil { 70 | panic("errors: Wrap|Wrapf called with nil error") 71 | } 72 | var ok bool 73 | if c, ok = err.(Chain); ok { 74 | c = append(c, newLink(nil, prefix, skipFrames)) 75 | } else { 76 | c = Chain{newLink(err, "", skipFrames)} 77 | for _, h := range helpers { 78 | if !h(c, err) { 79 | break 80 | } 81 | } 82 | if prefix != "" { 83 | c = append(c, &Link{Prefix: prefix, Source: c[0].Source}) 84 | } 85 | } 86 | return 87 | } 88 | 89 | // Cause extracts and returns the root wrapped error (the naked error with no additional information) 90 | func Cause(err error) error { 91 | for { 92 | switch t := err.(type) { 93 | case Chain: // fast path 94 | err = t[0].Err 95 | continue 96 | case unwrap: 97 | if unwrappedErr := t.Unwrap(); unwrappedErr != nil { 98 | err = unwrappedErr 99 | continue 100 | } 101 | } 102 | return err 103 | } 104 | } 105 | 106 | // HasType is a helper function that will recurse up from the root error and check that the provided type 107 | // is present using an equality check 108 | func HasType(err error, typ string) bool { 109 | for { 110 | switch t := err.(type) { 111 | case Chain: 112 | for i := len(t) - 1; i >= 0; i-- { 113 | for j := 0; j < len(t[i].Types); j++ { 114 | if t[i].Types[j] == typ { 115 | return true 116 | } 117 | } 118 | } 119 | err = t[0].Err 120 | continue 121 | case unwrap: 122 | err = t.Unwrap() 123 | continue 124 | } 125 | return false 126 | } 127 | } 128 | 129 | // LookupTag recursively searches for the provided tag and returns its value or nil 130 | func LookupTag(err error, key string) any { 131 | for { 132 | switch t := err.(type) { 133 | case Chain: 134 | for i := len(t) - 1; i >= 0; i-- { 135 | for j := 0; j < len(t[i].Tags); j++ { 136 | if t[i].Tags[j].Key == key { 137 | return t[i].Tags[j].Value 138 | } 139 | } 140 | } 141 | err = t[0].Err 142 | continue 143 | case unwrap: 144 | err = t.Unwrap() 145 | continue 146 | } 147 | return nil 148 | } 149 | } 150 | 151 | // Is allows this library to be a drop-in replacement to the std library. 152 | // 153 | // Is reports whether any error in the error chain matches target. 154 | // 155 | // The chain consists of err itself followed by the sequence of errors obtained by 156 | // repeatedly calling Unwrap. 157 | // 158 | // An error is considered to match a target if it is equal to that target or if 159 | // it implements a method Is(error) bool such that Is(target) returns true. 160 | // 161 | // An error type might provide an Is method, so it can be treated as equivalent 162 | // to an existing error. For example, if MyError defines 163 | // 164 | // func (m MyError) Is(target error) bool { return target == os.ErrExist } 165 | // 166 | // then Is(MyError{}, os.ErrExist) returns true. See syscall.Errno.Is for 167 | // an example in the standard library. 168 | func Is(err, target error) bool { 169 | return stderrors.Is(err, target) 170 | } 171 | 172 | // As is to allow this library to be a drop-in replacement to the std library. 173 | // 174 | // As finds the first error in the error chain that matches target, and if so, sets 175 | // target to that error value and returns true. Otherwise, it returns false. 176 | // 177 | // The chain consists of err itself followed by the sequence of errors obtained by 178 | // repeatedly calling Unwrap. 179 | // 180 | // An error matches target if the error's concrete value is assignable to the value 181 | // pointed to by target, or if the error has a method As(any) bool such that 182 | // As(target) returns true. In the latter case, the As method is responsible for 183 | // setting target. 184 | // 185 | // An error type might provide an As method, so it can be treated as if it were 186 | // a different error type. 187 | // 188 | // As panics if target is not a non-nil pointer to either a type that implements 189 | // error, or to any interface type. 190 | func As(err error, target any) bool { 191 | return stderrors.As(err, target) 192 | } 193 | -------------------------------------------------------------------------------- /errors_go1.20.go: -------------------------------------------------------------------------------- 1 | //go:build go1.20 2 | // +build go1.20 3 | 4 | package errors 5 | 6 | import stderrors "errors" 7 | 8 | // Join allows this library to be a drop-in replacement to the std library. 9 | // 10 | // Join returns an error that wraps the given errors. Any nil error values are discarded. 11 | // Join returns nil if every value in errs is nil. The error formats as the concatenation of the strings obtained 12 | // by calling the Error method of each element of errs, with a newline between each string. 13 | // 14 | // A non-nil error returned by Join implements the Unwrap() []error method. 15 | // 16 | // It is the responsibility of the caller to then check for nil and wrap this error if desired. 17 | func Join(errs ...error) error { 18 | return stderrors.Join(errs...) 19 | } 20 | -------------------------------------------------------------------------------- /errors_go1.20_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.20 2 | // +build go1.20 3 | 4 | package errors 5 | 6 | import ( 7 | "io" 8 | "testing" 9 | ) 10 | 11 | func TestJoin(t *testing.T) { 12 | err1 := io.EOF 13 | err2 := io.ErrUnexpectedEOF 14 | 15 | err := Join(err1, err2) 16 | innerErr, ok := err.(interface{ Unwrap() []error }) 17 | if !ok { 18 | t.Fatalf("expected Join to return an error that implements Unwrap() []error") 19 | } 20 | errs := innerErr.Unwrap() 21 | if len(errs) != 2 { 22 | t.Fatalf("expected Join to return an error that implements Unwrap() []error to return 2 errors") 23 | } 24 | if !Is(errs[0], io.EOF) { 25 | t.Fatalf("expected Join to return an error that implements Unwrap() []error to return io.EOF") 26 | } 27 | if !Is(errs[1], io.ErrUnexpectedEOF) { 28 | t.Fatalf("expected Join to return an error that implements Unwrap() []error to return io.ErrUnexpectedEOF") 29 | } 30 | 31 | // test wrapping and then unwrapping with Chain 32 | err = Wrap(err, "my test wrapped error") 33 | if !Is(err, io.EOF) { 34 | t.Fatalf("expected wrapped error to traverse into joined inner error EOF") 35 | } 36 | if !Is(err, io.ErrUnexpectedEOF) { 37 | t.Fatalf("expected wrapped error to traverse into joined inner error ErrUnexpectedEOF") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | stderrors "errors" 5 | "fmt" 6 | "io" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestWrap(t *testing.T) { 12 | defaultErr := fmt.Errorf("this is an %s", "error") 13 | testWrapper := func(err error, prefix string) Chain { 14 | return WrapSkipFrames(err, prefix, 1) 15 | } 16 | 17 | err0 := New("42") 18 | err1 := Wrap(defaultErr, "prefix 1") 19 | err2 := err1.Wrap("prefix 2") 20 | err3 := testWrapper(err2, "prefix 3") 21 | err4 := Newf("this is an %s", "error") 22 | err5 := Wrapf(defaultErr, "this is an %s", "error") 23 | 24 | tests := []struct { 25 | err Chain 26 | pre string 27 | suf string 28 | }{ 29 | { 30 | err: err0, 31 | pre: "TestWrap: ", 32 | suf: "errors_test.go:17", 33 | }, 34 | { 35 | err: err1, 36 | pre: "TestWrap: ", 37 | suf: "errors_test.go:18", 38 | }, 39 | { 40 | err: err2, 41 | pre: "TestWrap: ", 42 | suf: "errors_test.go:19", 43 | }, 44 | { 45 | err: err3, 46 | pre: "TestWrap: ", 47 | suf: "errors_test.go:20", 48 | }, 49 | { 50 | err: err4, 51 | pre: "TestWrap: ", 52 | suf: "errors_test.go:21", 53 | }, 54 | { 55 | err: err5, 56 | pre: "TestWrap: ", 57 | suf: "errors_test.go:22", 58 | }, 59 | } 60 | 61 | for i, tt := range tests { 62 | link := tt.err.current() 63 | source := fmt.Sprintf("%s: %s:%d", link.Source.Function(), link.Source.File(), link.Source.Line()) 64 | if !strings.HasSuffix(source, tt.suf) || !strings.HasPrefix(source, tt.pre) { 65 | t.Fatalf("IDX: %d want %s%s got %s", i, tt.pre, tt.suf, source) 66 | } 67 | } 68 | } 69 | 70 | func TestUnwrap(t *testing.T) { 71 | defaultErr := stderrors.New("this is an error") 72 | err := fmt.Errorf("std wrapped: %w", defaultErr) 73 | assertError(t, err) 74 | err = Wrap(defaultErr, "prefix") 75 | assertError(t, err) 76 | err = Wrap(err, "prefix2") 77 | assertError(t, err) 78 | err = fmt.Errorf("wrapping Chain: %w", err) 79 | assertError(t, err) 80 | err = fmt.Errorf("wrapping err again: %w", err) 81 | assertError(t, err) 82 | err = Wrap(err, "wrapping std with chain") 83 | 84 | for { 85 | switch t := err.(type) { 86 | case unwrap: 87 | if unwrappedErr := t.Unwrap(); unwrappedErr != nil { 88 | err = unwrappedErr 89 | continue 90 | } 91 | } 92 | break 93 | } 94 | expect := defaultErr.Error() 95 | if err.Error() != expect { 96 | t.Fatalf("want %s got %s", expect, err.Error()) 97 | } 98 | } 99 | 100 | func assertError(t *testing.T, err error) { 101 | if err == nil { 102 | t.Fatal("err is nil") 103 | } 104 | } 105 | 106 | func TestTags(t *testing.T) { 107 | tests := []struct { 108 | err string 109 | tags []Tag 110 | }{ 111 | { 112 | err: "key=value key2=value2", 113 | tags: []Tag{T("key", "value"), T("key2", "value2")}, 114 | }, 115 | } 116 | 117 | defaultErr := fmt.Errorf("this is an %s", "error") 118 | 119 | for i, tt := range tests { 120 | err := Wrap(defaultErr, "prefix").AddTags(tt.tags...) 121 | if !strings.HasSuffix(err.Error(), tt.err) { 122 | t.Fatalf("IDX: %d want %s got %s", i, tt.err, err.Error()) 123 | } 124 | } 125 | } 126 | 127 | func TestTypes(t *testing.T) { 128 | tests := []struct { 129 | err string 130 | tags []Tag 131 | types []string 132 | }{ 133 | { 134 | err: "types=Permanent,InternalError", 135 | tags: []Tag{T("key", "value"), T("key2", "value2")}, 136 | types: []string{"Permanent", "InternalError"}, 137 | }, 138 | } 139 | 140 | defaultErr := fmt.Errorf("this is an %s", "error") 141 | 142 | for i, tt := range tests { 143 | err := Wrap(defaultErr, "prefix").AddTags(tt.tags...).AddTypes(tt.types...) 144 | if !strings.HasSuffix(err.Error(), tt.err) { 145 | t.Fatalf("IDX: %d want %s got %s", i, tt.err, err.Error()) 146 | } 147 | } 148 | } 149 | 150 | func TestHasType(t *testing.T) { 151 | tests := []struct { 152 | name string 153 | typ string 154 | err error 155 | }{ 156 | { 157 | name: "basic types", 158 | typ: "Permanent", 159 | err: Wrap(fmt.Errorf("this is an %s", "error"), "prefix").AddTypes("Permanent", "internalError"), 160 | }, 161 | { 162 | name: "std wrapped", 163 | typ: "MyType", 164 | err: fmt.Errorf("std wrapped %w", New("base error").AddTypes("MyType")), 165 | }, 166 | } 167 | 168 | for i, tc := range tests { 169 | tc := tc 170 | i := i 171 | t.Run(tc.name, func(t *testing.T) { 172 | t.Parallel() 173 | if !HasType(tc.err, tc.typ) { 174 | t.Fatalf("IDX: %d want %t got %t", i, true, false) 175 | } 176 | }) 177 | } 178 | } 179 | 180 | func TestCause(t *testing.T) { 181 | defaultErr := fmt.Errorf("this is an %s", "error") 182 | err := Wrap(defaultErr, "prefix") 183 | err = Wrap(err, "prefix2") 184 | cause := Cause(err) 185 | expect := "this is an error" 186 | if cause.Error() != expect { 187 | t.Fatalf("want %s got %s", expect, cause.Error()) 188 | } 189 | } 190 | 191 | func TestCause2(t *testing.T) { 192 | err := Wrap(io.EOF, "prefix") 193 | err = Wrap(err, "prefix2") 194 | 195 | if Cause(err) != io.EOF { 196 | t.Fatalf("want %t got %t", true, false) 197 | } 198 | cause := Cause(err) 199 | if Cause(cause) != io.EOF { 200 | t.Fatalf("want %t got %t", true, false) 201 | } 202 | if Cause(io.EOF) != io.EOF { 203 | t.Fatalf("want %t got %t", true, false) 204 | } 205 | } 206 | 207 | func TestCauseStdErrorsMixed(t *testing.T) { 208 | defaultErr := stderrors.New("this is an error") 209 | err := fmt.Errorf("std wrapped: %w", defaultErr) 210 | assertError(t, err) 211 | err = Wrap(defaultErr, "prefix") 212 | assertError(t, err) 213 | err = Wrap(err, "prefix2") 214 | assertError(t, err) 215 | err = fmt.Errorf("wrapping Chain: %w", err) 216 | assertError(t, err) 217 | err = fmt.Errorf("wrapping err again: %w", err) 218 | assertError(t, err) 219 | err = Wrap(err, "wrapping std with chain") 220 | cause := Cause(err) 221 | expect := defaultErr.Error() 222 | if cause.Error() != expect { 223 | t.Fatalf("want %s got %s", expect, cause.Error()) 224 | } 225 | } 226 | 227 | func TestHelpers(t *testing.T) { 228 | fn := func(w Chain, _ error) (cont bool) { 229 | _ = w.AddTypes("Test").AddTags(T("test", "tag")).AddTag("foo", "bar") 230 | return false 231 | } 232 | RegisterHelper(fn) 233 | 234 | err := Wrap(io.EOF, "prefix") 235 | if !HasType(err, "Test") { 236 | t.Errorf("Expected to have type 'Test'") 237 | } 238 | } 239 | 240 | func TestLookupTag(t *testing.T) { 241 | key := "Key" 242 | value := "Value" 243 | 244 | tests := []struct { 245 | name string 246 | err error 247 | key string 248 | value any 249 | }{ 250 | { 251 | name: "basic wrap", 252 | err: Wrap(io.EOF, "prefix").AddTag(key, value), 253 | }, 254 | { 255 | name: "double wrapped", 256 | err: Wrap(Wrap(io.EOF, "prefix").AddTag(key, value), "wrapped"), 257 | }, 258 | { 259 | name: "std lib wrapped", 260 | err: fmt.Errorf("wrapped %w", Wrap(io.EOF, "prefix").AddTag(key, value)), 261 | }, 262 | } 263 | for _, tc := range tests { 264 | tc := tc 265 | t.Run(tc.name, func(t *testing.T) { 266 | t.Parallel() 267 | if LookupTag(tc.err, key) != value { 268 | t.Fatalf("want '%s' got '%v'", value, LookupTag(tc.err, key)) 269 | } 270 | }) 271 | } 272 | } 273 | 274 | func TestIs(t *testing.T) { 275 | defaultErr := io.EOF 276 | err := fmt.Errorf("std wrapped: %w", defaultErr) 277 | assertError(t, err) 278 | err = Wrap(defaultErr, "prefix") 279 | assertError(t, err) 280 | err = Wrap(err, "prefix2") 281 | assertError(t, err) 282 | err = fmt.Errorf("wrapping Chain: %w", err) 283 | assertError(t, err) 284 | err = fmt.Errorf("wrapping err again: %w", err) 285 | assertError(t, err) 286 | err = Wrap(err, "wrapping std with chain") 287 | if !Is(err, io.EOF) { 288 | t.Fatal("want true got false") 289 | } 290 | } 291 | 292 | type myErrorType struct { 293 | msg string 294 | } 295 | 296 | func (e *myErrorType) Error() string { 297 | return e.msg 298 | } 299 | 300 | func TestAs(t *testing.T) { 301 | defaultErr := &myErrorType{msg: "my error type"} 302 | err := fmt.Errorf("std wrapped: %w", defaultErr) 303 | assertError(t, err) 304 | err = Wrap(defaultErr, "prefix") 305 | assertError(t, err) 306 | err = Wrap(err, "prefix2") 307 | assertError(t, err) 308 | err = fmt.Errorf("wrapping Chain: %w", err) 309 | assertError(t, err) 310 | err = fmt.Errorf("wrapping err again: %w", err) 311 | assertError(t, err) 312 | err = Wrap(err, "wrapping std with chain") 313 | 314 | var myErr *myErrorType 315 | if !As(err, &myErr) { 316 | t.Fatal("want true got false") 317 | } 318 | } 319 | 320 | func TestCustomFormatFn(t *testing.T) { 321 | RegisterErrorFormatFn(func(c Chain) (s string) { 322 | return c[0].Err.Error() 323 | }) 324 | err := io.EOF 325 | err = Wrap(err, "my error prefix") 326 | if err.Error() != "EOF" { 327 | t.Errorf("Expected output of 'EOF'") 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-playground/errors/v5 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.45.27 7 | github.com/go-playground/pkg/v5 v5.21.3 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go v1.45.27 h1:b+zOTPkAG4i2RvqPdHxkJZafmhhVaVHBp4r41Tu4I6U= 2 | github.com/aws/aws-sdk-go v1.45.27/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/go-playground/pkg/v5 v5.21.3 h1:1IVy0eupI5kht6L6zaAqTEvjs00zLkG28ictNkoN1wE= 5 | github.com/go-playground/pkg/v5 v5.21.3/go.mod h1:UgHNntEQnMJSygw2O2RQ3LAB0tprx81K90c/pOKh7cU= 6 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 7 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 8 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 11 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 12 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 13 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 14 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 15 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 16 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 17 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 18 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 19 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 20 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 21 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 22 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 23 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 24 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 25 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 26 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 27 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 28 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 29 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 30 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 31 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 32 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 33 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 34 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 35 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 36 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 37 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 38 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 39 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 40 | -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | // Helper is a function which will automatically extract Type and Tag information based on the supplied err and 4 | // add it to the supplied *Link error; this can be used independently or by registering using errors.RegisterHelper(...), 5 | // which will run the registered helper every time errors.Wrap(...) is called. 6 | type Helper func(Chain, error) bool 7 | -------------------------------------------------------------------------------- /helpers/awserrors/awserrors.go: -------------------------------------------------------------------------------- 1 | package awserrors 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/awserr" 5 | "github.com/go-playground/errors/v5" 6 | ) 7 | 8 | const ( 9 | transient = "Transient" 10 | ) 11 | 12 | func init() { 13 | errors.RegisterHelper(AWSErrors) 14 | } 15 | 16 | // AWSErrors helps classify io related errors 17 | func AWSErrors(c errors.Chain, err error) (cont bool) { 18 | switch e := err.(type) { 19 | case awserr.BatchedErrors: 20 | _ = c.AddTypes(transient, "Batch") 21 | return 22 | 23 | case awserr.RequestFailure: 24 | _ = c.AddTypes(transient, "Request").AddTags( 25 | errors.T("status_code", e.StatusCode()), 26 | errors.T("request_id", e.RequestID()), 27 | ) 28 | return 29 | 30 | case awserr.Error: 31 | _ = c.AddTypes("General", "Error").AddTags(errors.T("aws_error_code", e.Code())) 32 | return 33 | } 34 | return true 35 | } 36 | -------------------------------------------------------------------------------- /helpers/ioerrors/ioerrors.go: -------------------------------------------------------------------------------- 1 | package ioerrors 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/go-playground/errors/v5" 7 | ) 8 | 9 | func init() { 10 | errors.RegisterHelper(IOErrors) 11 | } 12 | 13 | // IOErrors helps classify io related errors 14 | func IOErrors(c errors.Chain, err error) (cont bool) { 15 | switch err { 16 | case io.EOF: 17 | _ = c.AddTypes("io") 18 | return 19 | case io.ErrClosedPipe: 20 | _ = c.AddTypes("Permanent", "io") 21 | return 22 | case io.ErrNoProgress: 23 | _ = c.AddTypes("Permanent", "io") 24 | return 25 | case io.ErrShortBuffer: 26 | _ = c.AddTypes("Permanent", "io") 27 | return 28 | case io.ErrShortWrite: 29 | _ = c.AddTypes("Permanent", "io") 30 | return 31 | case io.ErrUnexpectedEOF: 32 | _ = c.AddTypes("Transient", "io") 33 | return 34 | } 35 | return true 36 | } 37 | -------------------------------------------------------------------------------- /helpers/neterrors/neterrors.go: -------------------------------------------------------------------------------- 1 | package neterrors 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/go-playground/errors/v5" 7 | ) 8 | 9 | const ( 10 | permanent = "Permanent" 11 | transient = "Transient" 12 | ) 13 | 14 | func init() { 15 | errors.RegisterHelper(NETErrors) 16 | } 17 | 18 | // NETErrors helps classify io related errors 19 | func NETErrors(c errors.Chain, err error) (cont bool) { 20 | switch e := err.(type) { 21 | case *net.AddrError: 22 | tp := permanent 23 | if e.Temporary() { 24 | tp = transient 25 | } 26 | _ = c.AddTypes(tp, "net").AddTags( 27 | errors.T("addr", e.Addr), 28 | errors.T("is_timeout", e.Timeout()), 29 | errors.T("is_temporary", e.Temporary()), 30 | ) 31 | return false 32 | 33 | case *net.DNSError: 34 | tp := permanent 35 | if e.Temporary() { 36 | tp = transient 37 | } 38 | _ = c.AddTypes(tp, "net").AddTags( 39 | errors.T("name", e.Name), 40 | errors.T("server", e.Server), 41 | errors.T("is_timeout", e.Timeout()), 42 | errors.T("is_temporary", e.Temporary()), 43 | ) 44 | return false 45 | 46 | case *net.ParseError: 47 | _ = c.AddTypes(permanent, "net").AddTags( 48 | errors.T("type", e.Type), 49 | errors.T("text", e.Text), 50 | ) 51 | return false 52 | 53 | case *net.OpError: 54 | tp := permanent 55 | if e.Temporary() { 56 | tp = transient 57 | } 58 | _ = c.AddTypes(tp, "net").AddTags( 59 | errors.T("op", e.Op), 60 | errors.T("net", e.Net), 61 | errors.T("addr", e.Addr), 62 | errors.T("local_addr", e.Source), 63 | errors.T("is_timeout", e.Timeout()), 64 | errors.T("is_temporary", e.Temporary()), 65 | ) 66 | return false 67 | case net.UnknownNetworkError: 68 | tp := permanent 69 | if e.Temporary() { 70 | tp = transient 71 | } 72 | _ = c.AddTypes(tp, "net").AddTags( 73 | errors.T("is_timeout", e.Timeout()), 74 | errors.T("is_temporary", e.Temporary()), 75 | ) 76 | } 77 | 78 | switch err { 79 | case net.ErrWriteToConnected: 80 | _ = c.AddTypes(transient, "net") 81 | return false 82 | } 83 | return true 84 | } 85 | --------------------------------------------------------------------------------