├── .codecov.yml ├── .github └── workflows │ ├── fossa.yaml │ └── go.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── Makefile ├── README.md ├── appendinvoke_example_test.go ├── benchmarks_test.go ├── error.go ├── error_ext_test.go ├── error_test.go ├── example_test.go ├── go.mod ├── go.sum └── tools ├── go.mod ├── go.sum └── tools.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 80..100 3 | round: down 4 | precision: 2 5 | 6 | status: 7 | project: # measuring the overall project coverage 8 | default: # context, you can create multiple ones with custom titles 9 | enabled: yes # must be yes|true to enable this status 10 | target: 100 # specify the target coverage for each commit status 11 | # option: "auto" (must increase from parent commit or pull request base) 12 | # option: "X%" a static target percentage to hit 13 | if_not_found: success # if parent is not found report status as success, error, or failure 14 | if_ci_failed: error # if ci fails report status as success, error, or failure 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/fossa.yaml: -------------------------------------------------------------------------------- 1 | name: FOSSA Analysis 2 | on: push 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | 9 | build: 10 | runs-on: ubuntu-latest 11 | if: github.repository_owner == 'uber-go' 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | 16 | - name: FOSSA analysis 17 | uses: fossas/fossa-action@v1 18 | with: 19 | api-key: ${{ secrets.FOSSA_API_KEY }} 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: ['*'] 6 | tags: ['v*'] 7 | pull_request: 8 | branches: ['*'] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | 15 | build: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | go: ["1.21.x", "1.22.x"] 20 | include: 21 | - go: 1.22.x 22 | latest: true 23 | 24 | steps: 25 | - name: Setup Go 26 | uses: actions/setup-go@v5 27 | with: 28 | go-version: ${{ matrix.go }} 29 | 30 | - name: Checkout code 31 | uses: actions/checkout@v4 32 | 33 | - name: Load cached dependencies 34 | uses: actions/cache@v4 35 | with: 36 | path: ~/go/pkg/mod 37 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 38 | restore-keys: | 39 | ${{ runner.os }}-go- 40 | 41 | - name: Download Dependencies 42 | run: go mod download 43 | 44 | - name: Lint 45 | if: matrix.latest 46 | run: make lint 47 | 48 | - name: Test 49 | run: make cover 50 | 51 | - name: Upload coverage to codecov.io 52 | uses: codecov/codecov-action@v4 53 | env: 54 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | cover.html 3 | cover.out 4 | /bin 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Releases 2 | ======== 3 | 4 | v1.11.0 (2023-03-28) 5 | ==================== 6 | - `Errors` now supports any error that implements multiple-error 7 | interface. 8 | - Add `Every` function to allow checking if all errors in the chain 9 | satisfies `errors.Is` against the target error. 10 | 11 | v1.10.0 (2023-03-08) 12 | ==================== 13 | 14 | - Comply with Go 1.20's multiple-error interface. 15 | - Drop Go 1.18 support. 16 | Per the support policy, only Go 1.19 and 1.20 are supported now. 17 | - Drop all non-test external dependencies. 18 | 19 | v1.9.0 (2022-12-12) 20 | =================== 21 | 22 | - Add `AppendFunc` that allow passsing functions to similar to 23 | `AppendInvoke`. 24 | 25 | - Bump up yaml.v3 dependency to 3.0.1. 26 | 27 | v1.8.0 (2022-02-28) 28 | =================== 29 | 30 | - `Combine`: perform zero allocations when there are no errors. 31 | 32 | 33 | v1.7.0 (2021-05-06) 34 | =================== 35 | 36 | - Add `AppendInvoke` to append into errors from `defer` blocks. 37 | 38 | 39 | v1.6.0 (2020-09-14) 40 | =================== 41 | 42 | - Actually drop library dependency on development-time tooling. 43 | 44 | 45 | v1.5.0 (2020-02-24) 46 | =================== 47 | 48 | - Drop library dependency on development-time tooling. 49 | 50 | 51 | v1.4.0 (2019-11-04) 52 | =================== 53 | 54 | - Add `AppendInto` function to more ergonomically build errors inside a 55 | loop. 56 | 57 | 58 | v1.3.0 (2019-10-29) 59 | =================== 60 | 61 | - Switch to Go modules. 62 | 63 | 64 | v1.2.0 (2019-09-26) 65 | =================== 66 | 67 | - Support extracting and matching against wrapped errors with `errors.As` 68 | and `errors.Is`. 69 | 70 | 71 | v1.1.0 (2017-06-30) 72 | =================== 73 | 74 | - Added an `Errors(error) []error` function to extract the underlying list of 75 | errors for a multierr error. 76 | 77 | 78 | v1.0.0 (2017-05-31) 79 | =================== 80 | 81 | No changes since v0.2.0. This release is committing to making no breaking 82 | changes to the current API in the 1.X series. 83 | 84 | 85 | v0.2.0 (2017-04-11) 86 | =================== 87 | 88 | - Repeatedly appending to the same error is now faster due to fewer 89 | allocations. 90 | 91 | 92 | v0.1.0 (2017-31-03) 93 | =================== 94 | 95 | - Initial release 96 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2021 Uber Technologies, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Directory to put `go install`ed binaries in. 2 | export GOBIN ?= $(shell pwd)/bin 3 | 4 | GO_FILES := $(shell \ 5 | find . '(' -path '*/.*' -o -path './vendor' ')' -prune \ 6 | -o -name '*.go' -print | cut -b3-) 7 | 8 | .PHONY: build 9 | build: 10 | go build ./... 11 | 12 | .PHONY: test 13 | test: 14 | go test -race ./... 15 | 16 | .PHONY: gofmt 17 | gofmt: 18 | $(eval FMT_LOG := $(shell mktemp -t gofmt.XXXXX)) 19 | @gofmt -e -s -l $(GO_FILES) > $(FMT_LOG) || true 20 | @[ ! -s "$(FMT_LOG)" ] || (echo "gofmt failed:" | cat - $(FMT_LOG) && false) 21 | 22 | .PHONY: golint 23 | golint: 24 | @cd tools && go install golang.org/x/lint/golint 25 | @$(GOBIN)/golint ./... 26 | 27 | .PHONY: staticcheck 28 | staticcheck: 29 | @cd tools && go install honnef.co/go/tools/cmd/staticcheck 30 | @$(GOBIN)/staticcheck ./... 31 | 32 | .PHONY: lint 33 | lint: gofmt golint staticcheck 34 | 35 | .PHONY: cover 36 | cover: 37 | go test -race -coverprofile=cover.out -coverpkg=./... -v ./... 38 | go tool cover -html=cover.out -o cover.html 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # multierr [![GoDoc][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov] 2 | 3 | `multierr` allows combining one or more Go `error`s together. 4 | 5 | ## Features 6 | 7 | - **Idiomatic**: 8 | multierr follows best practices in Go, and keeps your code idiomatic. 9 | - It keeps the underlying error type hidden, 10 | allowing you to deal in `error` values exclusively. 11 | - It provides APIs to safely append into an error from a `defer` statement. 12 | - **Performant**: 13 | multierr is optimized for performance: 14 | - It avoids allocations where possible. 15 | - It utilizes slice resizing semantics to optimize common cases 16 | like appending into the same error object from a loop. 17 | - **Interoperable**: 18 | multierr interoperates with the Go standard library's error APIs seamlessly: 19 | - The `errors.Is` and `errors.As` functions *just work*. 20 | - **Lightweight**: 21 | multierr comes with virtually no dependencies. 22 | 23 | ## Installation 24 | 25 | ```bash 26 | go get -u go.uber.org/multierr@latest 27 | ``` 28 | 29 | ## Status 30 | 31 | Stable: No breaking changes will be made before 2.0. 32 | 33 | ------------------------------------------------------------------------------- 34 | 35 | Released under the [MIT License]. 36 | 37 | [MIT License]: LICENSE.txt 38 | [doc-img]: https://pkg.go.dev/badge/go.uber.org/multierr 39 | [doc]: https://pkg.go.dev/go.uber.org/multierr 40 | [ci-img]: https://github.com/uber-go/multierr/actions/workflows/go.yml/badge.svg 41 | [cov-img]: https://codecov.io/gh/uber-go/multierr/branch/master/graph/badge.svg 42 | [ci]: https://github.com/uber-go/multierr/actions/workflows/go.yml 43 | [cov]: https://codecov.io/gh/uber-go/multierr 44 | -------------------------------------------------------------------------------- /appendinvoke_example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package multierr_test 22 | 23 | import ( 24 | "fmt" 25 | "log" 26 | "os" 27 | "path/filepath" 28 | 29 | "go.uber.org/multierr" 30 | ) 31 | 32 | func ExampleAppendInvoke() { 33 | if err := run(); err != nil { 34 | log.Fatal(err) 35 | } 36 | } 37 | 38 | func run() (err error) { 39 | dir, err := os.MkdirTemp("", "multierr") 40 | // We create a temporary directory and defer its deletion when this 41 | // function returns. 42 | // 43 | // If we failed to delete the temporary directory, we append its 44 | // failure into the returned value with multierr.AppendInvoke. 45 | // 46 | // This uses a custom invoker that we implement below. 47 | defer multierr.AppendInvoke(&err, RemoveAll(dir)) 48 | 49 | path := filepath.Join(dir, "example.txt") 50 | f, err := os.Create(path) 51 | if err != nil { 52 | return err 53 | } 54 | // Similarly, we defer closing the open file when the function returns, 55 | // and appends its failure, if any, into the returned error. 56 | // 57 | // This uses the multierr.Close invoker included in multierr. 58 | defer multierr.AppendInvoke(&err, multierr.Close(f)) 59 | 60 | if _, err := fmt.Fprintln(f, "hello"); err != nil { 61 | return err 62 | } 63 | 64 | return nil 65 | } 66 | 67 | // RemoveAll is a multierr.Invoker that deletes the provided directory and all 68 | // of its contents. 69 | type RemoveAll string 70 | 71 | func (r RemoveAll) Invoke() error { 72 | return os.RemoveAll(string(r)) 73 | } 74 | -------------------------------------------------------------------------------- /benchmarks_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package multierr 22 | 23 | import ( 24 | "errors" 25 | "fmt" 26 | "testing" 27 | ) 28 | 29 | func BenchmarkAppend(b *testing.B) { 30 | errorTypes := []struct { 31 | name string 32 | err error 33 | }{ 34 | { 35 | name: "nil", 36 | err: nil, 37 | }, 38 | { 39 | name: "single error", 40 | err: errors.New("test"), 41 | }, 42 | { 43 | name: "multiple errors", 44 | err: appendN(nil, errors.New("err"), 10), 45 | }, 46 | } 47 | 48 | for _, initial := range errorTypes { 49 | for _, v := range errorTypes { 50 | msg := fmt.Sprintf("append %v to %v", v.name, initial.name) 51 | b.Run(msg, func(b *testing.B) { 52 | for _, appends := range []int{1, 2, 10} { 53 | b.Run(fmt.Sprint(appends), func(b *testing.B) { 54 | for i := 0; i < b.N; i++ { 55 | appendN(initial.err, v.err, appends) 56 | } 57 | }) 58 | } 59 | }) 60 | } 61 | } 62 | } 63 | 64 | func BenchmarkCombine(b *testing.B) { 65 | b.Run("inline 1", func(b *testing.B) { 66 | var x error 67 | for i := 0; i < b.N; i++ { 68 | Combine(x) 69 | } 70 | }) 71 | 72 | b.Run("inline 2", func(b *testing.B) { 73 | var x, y error 74 | for i := 0; i < b.N; i++ { 75 | Combine(x, y) 76 | } 77 | }) 78 | 79 | b.Run("inline 3 no error", func(b *testing.B) { 80 | var x, y, z error 81 | for i := 0; i < b.N; i++ { 82 | Combine(x, y, z) 83 | } 84 | }) 85 | 86 | b.Run("inline 3 one error", func(b *testing.B) { 87 | var x, y, z error 88 | z = fmt.Errorf("failed") 89 | for i := 0; i < b.N; i++ { 90 | Combine(x, y, z) 91 | } 92 | }) 93 | 94 | b.Run("inline 3 multiple errors", func(b *testing.B) { 95 | var x, y, z error 96 | z = fmt.Errorf("failed3") 97 | y = fmt.Errorf("failed2") 98 | x = fmt.Errorf("failed") 99 | for i := 0; i < b.N; i++ { 100 | Combine(x, y, z) 101 | } 102 | }) 103 | 104 | b.Run("slice 100 no errors", func(b *testing.B) { 105 | errs := make([]error, 100) 106 | for i := 0; i < b.N; i++ { 107 | Combine(errs...) 108 | } 109 | }) 110 | 111 | b.Run("slice 100 one error", func(b *testing.B) { 112 | errs := make([]error, 100) 113 | errs[len(errs)-1] = fmt.Errorf("failed") 114 | for i := 0; i < b.N; i++ { 115 | Combine(errs...) 116 | } 117 | }) 118 | 119 | b.Run("slice 100 multi error", func(b *testing.B) { 120 | errs := make([]error, 100) 121 | errs[0] = fmt.Errorf("failed1") 122 | errs[len(errs)-1] = fmt.Errorf("failed2") 123 | for i := 0; i < b.N; i++ { 124 | Combine(errs...) 125 | } 126 | }) 127 | } 128 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2023 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | // Package multierr allows combining one or more errors together. 22 | // 23 | // # Overview 24 | // 25 | // Errors can be combined with the use of the Combine function. 26 | // 27 | // multierr.Combine( 28 | // reader.Close(), 29 | // writer.Close(), 30 | // conn.Close(), 31 | // ) 32 | // 33 | // If only two errors are being combined, the Append function may be used 34 | // instead. 35 | // 36 | // err = multierr.Append(reader.Close(), writer.Close()) 37 | // 38 | // The underlying list of errors for a returned error object may be retrieved 39 | // with the Errors function. 40 | // 41 | // errors := multierr.Errors(err) 42 | // if len(errors) > 0 { 43 | // fmt.Println("The following errors occurred:", errors) 44 | // } 45 | // 46 | // # Appending from a loop 47 | // 48 | // You sometimes need to append into an error from a loop. 49 | // 50 | // var err error 51 | // for _, item := range items { 52 | // err = multierr.Append(err, process(item)) 53 | // } 54 | // 55 | // Cases like this may require knowledge of whether an individual instance 56 | // failed. This usually requires introduction of a new variable. 57 | // 58 | // var err error 59 | // for _, item := range items { 60 | // if perr := process(item); perr != nil { 61 | // log.Warn("skipping item", item) 62 | // err = multierr.Append(err, perr) 63 | // } 64 | // } 65 | // 66 | // multierr includes AppendInto to simplify cases like this. 67 | // 68 | // var err error 69 | // for _, item := range items { 70 | // if multierr.AppendInto(&err, process(item)) { 71 | // log.Warn("skipping item", item) 72 | // } 73 | // } 74 | // 75 | // This will append the error into the err variable, and return true if that 76 | // individual error was non-nil. 77 | // 78 | // See [AppendInto] for more information. 79 | // 80 | // # Deferred Functions 81 | // 82 | // Go makes it possible to modify the return value of a function in a defer 83 | // block if the function was using named returns. This makes it possible to 84 | // record resource cleanup failures from deferred blocks. 85 | // 86 | // func sendRequest(req Request) (err error) { 87 | // conn, err := openConnection() 88 | // if err != nil { 89 | // return err 90 | // } 91 | // defer func() { 92 | // err = multierr.Append(err, conn.Close()) 93 | // }() 94 | // // ... 95 | // } 96 | // 97 | // multierr provides the Invoker type and AppendInvoke function to make cases 98 | // like the above simpler and obviate the need for a closure. The following is 99 | // roughly equivalent to the example above. 100 | // 101 | // func sendRequest(req Request) (err error) { 102 | // conn, err := openConnection() 103 | // if err != nil { 104 | // return err 105 | // } 106 | // defer multierr.AppendInvoke(&err, multierr.Close(conn)) 107 | // // ... 108 | // } 109 | // 110 | // See [AppendInvoke] and [Invoker] for more information. 111 | // 112 | // NOTE: If you're modifying an error from inside a defer, you MUST use a named 113 | // return value for that function. 114 | // 115 | // # Advanced Usage 116 | // 117 | // Errors returned by Combine and Append MAY implement the following 118 | // method. 119 | // 120 | // // Returns a slice containing the underlying list of errors. 121 | // // 122 | // // This slice MUST NOT be modified by the caller. 123 | // Unwrap() []error 124 | // 125 | // Note that if you need access to list of errors behind a multierr error, you 126 | // should prefer using the [Errors] function. That said, if you need cheap 127 | // read-only access to the underlying errors slice, you can attempt to cast 128 | // the error to this interface. You MUST handle the failure case gracefully 129 | // because errors returned by Combine and Append are not guaranteed to 130 | // implement this interface. 131 | // 132 | // var errors []error 133 | // group, ok := err.(interface{ Unwrap() []error }) 134 | // if ok { 135 | // errors = group.Errors() 136 | // } else { 137 | // errors = []error{err} 138 | // } 139 | package multierr // import "go.uber.org/multierr" 140 | 141 | import ( 142 | "bytes" 143 | "errors" 144 | "fmt" 145 | "io" 146 | "strings" 147 | "sync" 148 | "sync/atomic" 149 | ) 150 | 151 | var ( 152 | // Separator for single-line error messages. 153 | _singlelineSeparator = []byte("; ") 154 | 155 | // Prefix for multi-line messages 156 | _multilinePrefix = []byte("the following errors occurred:") 157 | 158 | // Prefix for the first and following lines of an item in a list of 159 | // multi-line error messages. 160 | // 161 | // For example, if a single item is: 162 | // 163 | // foo 164 | // bar 165 | // 166 | // It will become, 167 | // 168 | // - foo 169 | // bar 170 | _multilineSeparator = []byte("\n - ") 171 | _multilineIndent = []byte(" ") 172 | ) 173 | 174 | // _bufferPool is a pool of bytes.Buffers. 175 | var _bufferPool = sync.Pool{ 176 | New: func() interface{} { 177 | return &bytes.Buffer{} 178 | }, 179 | } 180 | 181 | // errorGroup is the old interface defined by multierr for combined errors. 182 | // 183 | // It is deprecated in favor of the Go 1.20 multi-error interface. 184 | // However, we still need to implement and support it 185 | // for backward compatibility. 186 | type errorGroup interface { 187 | Errors() []error 188 | } 189 | 190 | // multipleErrors matches the Go 1.20 multi-error interface. 191 | type multipleErrors interface { 192 | Unwrap() []error 193 | } 194 | 195 | // Errors returns a slice containing zero or more errors that the supplied 196 | // error is composed of. If the error is nil, a nil slice is returned. 197 | // 198 | // err := multierr.Append(r.Close(), w.Close()) 199 | // errors := multierr.Errors(err) 200 | // 201 | // If the error is not composed of other errors, the returned slice contains 202 | // just the error that was passed in. 203 | // 204 | // Callers of this function are free to modify the returned slice. 205 | func Errors(err error) []error { 206 | return extractErrors(err) 207 | } 208 | 209 | // multiError is an error that holds one or more errors. 210 | // 211 | // An instance of this is guaranteed to be non-empty and flattened. That is, 212 | // none of the errors inside multiError are other multiErrors. 213 | // 214 | // multiError formats to a semi-colon delimited list of error messages with 215 | // %v and with a more readable multi-line format with %+v. 216 | type multiError struct { 217 | copyNeeded atomic.Bool 218 | errors []error 219 | } 220 | 221 | // Unwrap returns a list of errors wrapped by this multierr. 222 | // 223 | // This satisfies the Go 1.20 multi-error interface. 224 | func (merr *multiError) Unwrap() []error { 225 | return merr.Errors() 226 | } 227 | 228 | // Errors returns the list of underlying errors. 229 | // 230 | // This slice MUST NOT be modified. 231 | func (merr *multiError) Errors() []error { 232 | if merr == nil { 233 | return nil 234 | } 235 | return merr.errors 236 | } 237 | 238 | func (merr *multiError) Error() string { 239 | if merr == nil { 240 | return "" 241 | } 242 | 243 | buff := _bufferPool.Get().(*bytes.Buffer) 244 | buff.Reset() 245 | 246 | merr.writeSingleline(buff) 247 | 248 | result := buff.String() 249 | _bufferPool.Put(buff) 250 | return result 251 | } 252 | 253 | func (merr *multiError) Format(f fmt.State, c rune) { 254 | if c == 'v' && f.Flag('+') { 255 | merr.writeMultiline(f) 256 | } else { 257 | merr.writeSingleline(f) 258 | } 259 | } 260 | 261 | func (merr *multiError) writeSingleline(w io.Writer) { 262 | first := true 263 | for _, item := range merr.errors { 264 | if first { 265 | first = false 266 | } else { 267 | w.Write(_singlelineSeparator) 268 | } 269 | io.WriteString(w, item.Error()) 270 | } 271 | } 272 | 273 | func (merr *multiError) writeMultiline(w io.Writer) { 274 | w.Write(_multilinePrefix) 275 | for _, item := range merr.errors { 276 | w.Write(_multilineSeparator) 277 | writePrefixLine(w, _multilineIndent, fmt.Sprintf("%+v", item)) 278 | } 279 | } 280 | 281 | // Writes s to the writer with the given prefix added before each line after 282 | // the first. 283 | func writePrefixLine(w io.Writer, prefix []byte, s string) { 284 | first := true 285 | for len(s) > 0 { 286 | if first { 287 | first = false 288 | } else { 289 | w.Write(prefix) 290 | } 291 | 292 | idx := strings.IndexByte(s, '\n') 293 | if idx < 0 { 294 | idx = len(s) - 1 295 | } 296 | 297 | io.WriteString(w, s[:idx+1]) 298 | s = s[idx+1:] 299 | } 300 | } 301 | 302 | type inspectResult struct { 303 | // Number of top-level non-nil errors 304 | Count int 305 | 306 | // Total number of errors including multiErrors 307 | Capacity int 308 | 309 | // Index of the first non-nil error in the list. Value is meaningless if 310 | // Count is zero. 311 | FirstErrorIdx int 312 | 313 | // Whether the list contains at least one multiError 314 | ContainsMultiError bool 315 | } 316 | 317 | // Inspects the given slice of errors so that we can efficiently allocate 318 | // space for it. 319 | func inspect(errors []error) (res inspectResult) { 320 | first := true 321 | for i, err := range errors { 322 | if err == nil { 323 | continue 324 | } 325 | 326 | res.Count++ 327 | if first { 328 | first = false 329 | res.FirstErrorIdx = i 330 | } 331 | 332 | if merr, ok := err.(*multiError); ok { 333 | res.Capacity += len(merr.errors) 334 | res.ContainsMultiError = true 335 | } else { 336 | res.Capacity++ 337 | } 338 | } 339 | return 340 | } 341 | 342 | // fromSlice converts the given list of errors into a single error. 343 | func fromSlice(errors []error) error { 344 | // Don't pay to inspect small slices. 345 | switch len(errors) { 346 | case 0: 347 | return nil 348 | case 1: 349 | return errors[0] 350 | } 351 | 352 | res := inspect(errors) 353 | switch res.Count { 354 | case 0: 355 | return nil 356 | case 1: 357 | // only one non-nil entry 358 | return errors[res.FirstErrorIdx] 359 | case len(errors): 360 | if !res.ContainsMultiError { 361 | // Error list is flat. Make a copy of it 362 | // Otherwise "errors" escapes to the heap 363 | // unconditionally for all other cases. 364 | // This lets us optimize for the "no errors" case. 365 | out := append(([]error)(nil), errors...) 366 | return &multiError{errors: out} 367 | } 368 | } 369 | 370 | nonNilErrs := make([]error, 0, res.Capacity) 371 | for _, err := range errors[res.FirstErrorIdx:] { 372 | if err == nil { 373 | continue 374 | } 375 | 376 | if nested, ok := err.(*multiError); ok { 377 | nonNilErrs = append(nonNilErrs, nested.errors...) 378 | } else { 379 | nonNilErrs = append(nonNilErrs, err) 380 | } 381 | } 382 | 383 | return &multiError{errors: nonNilErrs} 384 | } 385 | 386 | // Combine combines the passed errors into a single error. 387 | // 388 | // If zero arguments were passed or if all items are nil, a nil error is 389 | // returned. 390 | // 391 | // Combine(nil, nil) // == nil 392 | // 393 | // If only a single error was passed, it is returned as-is. 394 | // 395 | // Combine(err) // == err 396 | // 397 | // Combine skips over nil arguments so this function may be used to combine 398 | // together errors from operations that fail independently of each other. 399 | // 400 | // multierr.Combine( 401 | // reader.Close(), 402 | // writer.Close(), 403 | // pipe.Close(), 404 | // ) 405 | // 406 | // If any of the passed errors is a multierr error, it will be flattened along 407 | // with the other errors. 408 | // 409 | // multierr.Combine(multierr.Combine(err1, err2), err3) 410 | // // is the same as 411 | // multierr.Combine(err1, err2, err3) 412 | // 413 | // The returned error formats into a readable multi-line error message if 414 | // formatted with %+v. 415 | // 416 | // fmt.Sprintf("%+v", multierr.Combine(err1, err2)) 417 | func Combine(errors ...error) error { 418 | return fromSlice(errors) 419 | } 420 | 421 | // Append appends the given errors together. Either value may be nil. 422 | // 423 | // This function is a specialization of Combine for the common case where 424 | // there are only two errors. 425 | // 426 | // err = multierr.Append(reader.Close(), writer.Close()) 427 | // 428 | // The following pattern may also be used to record failure of deferred 429 | // operations without losing information about the original error. 430 | // 431 | // func doSomething(..) (err error) { 432 | // f := acquireResource() 433 | // defer func() { 434 | // err = multierr.Append(err, f.Close()) 435 | // }() 436 | // 437 | // Note that the variable MUST be a named return to append an error to it from 438 | // the defer statement. See also [AppendInvoke]. 439 | func Append(left error, right error) error { 440 | switch { 441 | case left == nil: 442 | return right 443 | case right == nil: 444 | return left 445 | } 446 | 447 | if _, ok := right.(*multiError); !ok { 448 | if l, ok := left.(*multiError); ok && !l.copyNeeded.Swap(true) { 449 | // Common case where the error on the left is constantly being 450 | // appended to. 451 | errs := append(l.errors, right) 452 | return &multiError{errors: errs} 453 | } else if !ok { 454 | // Both errors are single errors. 455 | return &multiError{errors: []error{left, right}} 456 | } 457 | } 458 | 459 | // Either right or both, left and right, are multiErrors. Rely on usual 460 | // expensive logic. 461 | errors := [2]error{left, right} 462 | return fromSlice(errors[0:]) 463 | } 464 | 465 | // AppendInto appends an error into the destination of an error pointer and 466 | // returns whether the error being appended was non-nil. 467 | // 468 | // var err error 469 | // multierr.AppendInto(&err, r.Close()) 470 | // multierr.AppendInto(&err, w.Close()) 471 | // 472 | // The above is equivalent to, 473 | // 474 | // err := multierr.Append(r.Close(), w.Close()) 475 | // 476 | // As AppendInto reports whether the provided error was non-nil, it may be 477 | // used to build a multierr error in a loop more ergonomically. For example: 478 | // 479 | // var err error 480 | // for line := range lines { 481 | // var item Item 482 | // if multierr.AppendInto(&err, parse(line, &item)) { 483 | // continue 484 | // } 485 | // items = append(items, item) 486 | // } 487 | // 488 | // Compare this with a version that relies solely on Append: 489 | // 490 | // var err error 491 | // for line := range lines { 492 | // var item Item 493 | // if parseErr := parse(line, &item); parseErr != nil { 494 | // err = multierr.Append(err, parseErr) 495 | // continue 496 | // } 497 | // items = append(items, item) 498 | // } 499 | func AppendInto(into *error, err error) (errored bool) { 500 | if into == nil { 501 | // We panic if 'into' is nil. This is not documented above 502 | // because suggesting that the pointer must be non-nil may 503 | // confuse users into thinking that the error that it points 504 | // to must be non-nil. 505 | panic("misuse of multierr.AppendInto: into pointer must not be nil") 506 | } 507 | 508 | if err == nil { 509 | return false 510 | } 511 | *into = Append(*into, err) 512 | return true 513 | } 514 | 515 | // Every compares every error in the given err against the given target error 516 | // using [errors.Is], and returns true only if every comparison returned true. 517 | func Every(err error, target error) bool { 518 | for _, e := range extractErrors(err) { 519 | if !errors.Is(e, target) { 520 | return false 521 | } 522 | } 523 | return true 524 | } 525 | 526 | func extractErrors(err error) []error { 527 | if err == nil { 528 | return nil 529 | } 530 | 531 | // check if the given err is an Unwrapable error that 532 | // implements multipleErrors interface. 533 | eg, ok := err.(multipleErrors) 534 | if !ok { 535 | return []error{err} 536 | } 537 | 538 | return append(([]error)(nil), eg.Unwrap()...) 539 | } 540 | 541 | // Invoker is an operation that may fail with an error. Use it with 542 | // AppendInvoke to append the result of calling the function into an error. 543 | // This allows you to conveniently defer capture of failing operations. 544 | // 545 | // See also, [Close] and [Invoke]. 546 | type Invoker interface { 547 | Invoke() error 548 | } 549 | 550 | // Invoke wraps a function which may fail with an error to match the Invoker 551 | // interface. Use it to supply functions matching this signature to 552 | // AppendInvoke. 553 | // 554 | // For example, 555 | // 556 | // func processReader(r io.Reader) (err error) { 557 | // scanner := bufio.NewScanner(r) 558 | // defer multierr.AppendInvoke(&err, multierr.Invoke(scanner.Err)) 559 | // for scanner.Scan() { 560 | // // ... 561 | // } 562 | // // ... 563 | // } 564 | // 565 | // In this example, the following line will construct the Invoker right away, 566 | // but defer the invocation of scanner.Err() until the function returns. 567 | // 568 | // defer multierr.AppendInvoke(&err, multierr.Invoke(scanner.Err)) 569 | // 570 | // Note that the error you're appending to from the defer statement MUST be a 571 | // named return. 572 | type Invoke func() error 573 | 574 | // Invoke calls the supplied function and returns its result. 575 | func (i Invoke) Invoke() error { return i() } 576 | 577 | // Close builds an Invoker that closes the provided io.Closer. Use it with 578 | // AppendInvoke to close io.Closers and append their results into an error. 579 | // 580 | // For example, 581 | // 582 | // func processFile(path string) (err error) { 583 | // f, err := os.Open(path) 584 | // if err != nil { 585 | // return err 586 | // } 587 | // defer multierr.AppendInvoke(&err, multierr.Close(f)) 588 | // return processReader(f) 589 | // } 590 | // 591 | // In this example, multierr.Close will construct the Invoker right away, but 592 | // defer the invocation of f.Close until the function returns. 593 | // 594 | // defer multierr.AppendInvoke(&err, multierr.Close(f)) 595 | // 596 | // Note that the error you're appending to from the defer statement MUST be a 597 | // named return. 598 | func Close(closer io.Closer) Invoker { 599 | return Invoke(closer.Close) 600 | } 601 | 602 | // AppendInvoke appends the result of calling the given Invoker into the 603 | // provided error pointer. Use it with named returns to safely defer 604 | // invocation of fallible operations until a function returns, and capture the 605 | // resulting errors. 606 | // 607 | // func doSomething(...) (err error) { 608 | // // ... 609 | // f, err := openFile(..) 610 | // if err != nil { 611 | // return err 612 | // } 613 | // 614 | // // multierr will call f.Close() when this function returns and 615 | // // if the operation fails, its append its error into the 616 | // // returned error. 617 | // defer multierr.AppendInvoke(&err, multierr.Close(f)) 618 | // 619 | // scanner := bufio.NewScanner(f) 620 | // // Similarly, this scheduled scanner.Err to be called and 621 | // // inspected when the function returns and append its error 622 | // // into the returned error. 623 | // defer multierr.AppendInvoke(&err, multierr.Invoke(scanner.Err)) 624 | // 625 | // // ... 626 | // } 627 | // 628 | // NOTE: If used with a defer, the error variable MUST be a named return. 629 | // 630 | // Without defer, AppendInvoke behaves exactly like AppendInto. 631 | // 632 | // err := // ... 633 | // multierr.AppendInvoke(&err, mutltierr.Invoke(foo)) 634 | // 635 | // // ...is roughly equivalent to... 636 | // 637 | // err := // ... 638 | // multierr.AppendInto(&err, foo()) 639 | // 640 | // The advantage of the indirection introduced by Invoker is to make it easy 641 | // to defer the invocation of a function. Without this indirection, the 642 | // invoked function will be evaluated at the time of the defer block rather 643 | // than when the function returns. 644 | // 645 | // // BAD: This is likely not what the caller intended. This will evaluate 646 | // // foo() right away and append its result into the error when the 647 | // // function returns. 648 | // defer multierr.AppendInto(&err, foo()) 649 | // 650 | // // GOOD: This will defer invocation of foo unutil the function returns. 651 | // defer multierr.AppendInvoke(&err, multierr.Invoke(foo)) 652 | // 653 | // multierr provides a few Invoker implementations out of the box for 654 | // convenience. See [Invoker] for more information. 655 | func AppendInvoke(into *error, invoker Invoker) { 656 | AppendInto(into, invoker.Invoke()) 657 | } 658 | 659 | // AppendFunc is a shorthand for [AppendInvoke]. 660 | // It allows using function or method value directly 661 | // without having to wrap it into an [Invoker] interface. 662 | // 663 | // func doSomething(...) (err error) { 664 | // w, err := startWorker(...) 665 | // if err != nil { 666 | // return err 667 | // } 668 | // 669 | // // multierr will call w.Stop() when this function returns and 670 | // // if the operation fails, it appends its error into the 671 | // // returned error. 672 | // defer multierr.AppendFunc(&err, w.Stop) 673 | // } 674 | func AppendFunc(into *error, fn func() error) { 675 | AppendInvoke(into, Invoke(fn)) 676 | } 677 | -------------------------------------------------------------------------------- /error_ext_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package multierr_test 22 | 23 | import ( 24 | "errors" 25 | "os" 26 | "testing" 27 | 28 | "github.com/stretchr/testify/assert" 29 | "github.com/stretchr/testify/require" 30 | "go.uber.org/multierr" 31 | ) 32 | 33 | type errGreatSadness struct{ id int } 34 | 35 | func (errGreatSadness) Error() string { 36 | return "great sadness" 37 | } 38 | 39 | type errUnprecedentedFailure struct{ id int } 40 | 41 | func (errUnprecedentedFailure) Error() string { 42 | return "unprecedented failure" 43 | } 44 | 45 | func (e errUnprecedentedFailure) Unwrap() error { 46 | return errRootCause{e.id} 47 | } 48 | 49 | type errRootCause struct{ i int } 50 | 51 | func (errRootCause) Error() string { 52 | return "root cause" 53 | } 54 | 55 | func TestErrorsWrapping(t *testing.T) { 56 | err := multierr.Append( 57 | errGreatSadness{42}, 58 | errUnprecedentedFailure{43}, 59 | ) 60 | 61 | t.Run("left", func(t *testing.T) { 62 | t.Run("As", func(t *testing.T) { 63 | var got errGreatSadness 64 | require.True(t, errors.As(err, &got)) 65 | assert.Equal(t, 42, got.id) 66 | }) 67 | 68 | t.Run("Is", func(t *testing.T) { 69 | assert.False(t, errors.Is(err, errGreatSadness{41})) 70 | assert.True(t, errors.Is(err, errGreatSadness{42})) 71 | }) 72 | }) 73 | 74 | t.Run("right", func(t *testing.T) { 75 | t.Run("As", func(t *testing.T) { 76 | var got errUnprecedentedFailure 77 | require.True(t, errors.As(err, &got)) 78 | assert.Equal(t, 43, got.id) 79 | }) 80 | 81 | t.Run("Is", func(t *testing.T) { 82 | assert.False(t, errors.Is(err, errUnprecedentedFailure{42})) 83 | assert.True(t, errors.Is(err, errUnprecedentedFailure{43})) 84 | }) 85 | }) 86 | 87 | t.Run("top-level", func(t *testing.T) { 88 | t.Run("As", func(t *testing.T) { 89 | var got interface{ Errors() []error } 90 | require.True(t, errors.As(err, &got)) 91 | assert.Len(t, got.Errors(), 2) 92 | }) 93 | 94 | t.Run("Is", func(t *testing.T) { 95 | assert.True(t, errors.Is(err, err)) 96 | }) 97 | }) 98 | 99 | t.Run("root cause", func(t *testing.T) { 100 | t.Run("As", func(t *testing.T) { 101 | var got errRootCause 102 | require.True(t, errors.As(err, &got)) 103 | assert.Equal(t, 43, got.i) 104 | }) 105 | 106 | t.Run("Is", func(t *testing.T) { 107 | assert.False(t, errors.Is(err, errRootCause{42})) 108 | assert.True(t, errors.Is(err, errRootCause{43})) 109 | }) 110 | }) 111 | 112 | t.Run("mismatch", func(t *testing.T) { 113 | t.Run("As", func(t *testing.T) { 114 | var got *os.PathError 115 | assert.False(t, errors.As(err, &got)) 116 | }) 117 | 118 | t.Run("Is", func(t *testing.T) { 119 | assert.False(t, errors.Is(err, errors.New("great sadness"))) 120 | }) 121 | }) 122 | } 123 | 124 | func TestErrorsWrappingSameType(t *testing.T) { 125 | err := multierr.Combine( 126 | errGreatSadness{1}, 127 | errGreatSadness{2}, 128 | errGreatSadness{3}, 129 | ) 130 | 131 | t.Run("As returns first", func(t *testing.T) { 132 | var got errGreatSadness 133 | require.True(t, errors.As(err, &got)) 134 | assert.Equal(t, 1, got.id) 135 | }) 136 | 137 | t.Run("Is matches all", func(t *testing.T) { 138 | assert.True(t, errors.Is(err, errGreatSadness{1})) 139 | assert.True(t, errors.Is(err, errGreatSadness{2})) 140 | assert.True(t, errors.Is(err, errGreatSadness{3})) 141 | }) 142 | } 143 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2023 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package multierr 22 | 23 | import ( 24 | "errors" 25 | "fmt" 26 | "io" 27 | "sync" 28 | "testing" 29 | 30 | "github.com/stretchr/testify/assert" 31 | "github.com/stretchr/testify/require" 32 | ) 33 | 34 | // richFormatError is an error that prints a different output depending on 35 | // whether %v or %+v was used. 36 | type richFormatError struct{} 37 | 38 | func (r richFormatError) Error() string { 39 | return fmt.Sprint(r) 40 | } 41 | 42 | func (richFormatError) Format(f fmt.State, c rune) { 43 | if c == 'v' && f.Flag('+') { 44 | io.WriteString(f, "multiline\nmessage\nwith plus") 45 | } else { 46 | io.WriteString(f, "without plus") 47 | } 48 | } 49 | 50 | func appendN(initial, err error, n int) error { 51 | errs := initial 52 | for i := 0; i < n; i++ { 53 | errs = Append(errs, err) 54 | } 55 | return errs 56 | } 57 | 58 | func newMultiErr(errors ...error) error { 59 | return &multiError{errors: errors} 60 | } 61 | 62 | func TestEvery(t *testing.T) { 63 | myError1 := errors.New("woeful misfortune") 64 | myError2 := errors.New("worrisome travesty") 65 | 66 | for _, tt := range []struct { 67 | desc string 68 | giveErr error 69 | giveTarget error 70 | wantIs bool 71 | wantEvery bool 72 | }{ 73 | { 74 | desc: "all match", 75 | giveErr: newMultiErr(myError1, myError1, myError1), 76 | giveTarget: myError1, 77 | wantIs: true, 78 | wantEvery: true, 79 | }, 80 | { 81 | desc: "one matches", 82 | giveErr: newMultiErr(myError1, myError2), 83 | giveTarget: myError1, 84 | wantIs: true, 85 | wantEvery: false, 86 | }, 87 | { 88 | desc: "not multiErrs and non equal", 89 | giveErr: myError1, 90 | giveTarget: myError2, 91 | wantIs: false, 92 | wantEvery: false, 93 | }, 94 | { 95 | desc: "not multiErrs but equal", 96 | giveErr: myError1, 97 | giveTarget: myError1, 98 | wantIs: true, 99 | wantEvery: true, 100 | }, 101 | { 102 | desc: "not multiErr w multiErr target", 103 | giveErr: myError1, 104 | giveTarget: newMultiErr(myError1, myError1), 105 | wantIs: false, 106 | wantEvery: false, 107 | }, 108 | { 109 | desc: "multiErr w multiErr target", 110 | giveErr: newMultiErr(myError1, myError1), 111 | giveTarget: newMultiErr(myError1, myError1), 112 | wantIs: false, 113 | wantEvery: false, 114 | }, 115 | } { 116 | t.Run(tt.desc, func(t *testing.T) { 117 | assert.Equal(t, tt.wantIs, errors.Is(tt.giveErr, tt.giveTarget)) 118 | assert.Equal(t, tt.wantEvery, Every(tt.giveErr, tt.giveTarget)) 119 | }) 120 | } 121 | } 122 | 123 | func TestCombine(t *testing.T) { 124 | tests := []struct { 125 | // Input 126 | giveErrors []error 127 | 128 | // Resulting error 129 | wantError error 130 | 131 | // %+v and %v string representations 132 | wantMultiline string 133 | wantSingleline string 134 | }{ 135 | { 136 | giveErrors: nil, 137 | wantError: nil, 138 | }, 139 | { 140 | giveErrors: []error{}, 141 | wantError: nil, 142 | }, 143 | { 144 | giveErrors: []error{ 145 | errors.New("foo"), 146 | nil, 147 | newMultiErr( 148 | errors.New("bar"), 149 | ), 150 | nil, 151 | }, 152 | wantError: newMultiErr( 153 | errors.New("foo"), 154 | errors.New("bar"), 155 | ), 156 | wantMultiline: "the following errors occurred:\n" + 157 | " - foo\n" + 158 | " - bar", 159 | wantSingleline: "foo; bar", 160 | }, 161 | { 162 | giveErrors: []error{nil, nil, errors.New("great sadness"), nil}, 163 | wantError: errors.New("great sadness"), 164 | wantMultiline: "great sadness", 165 | wantSingleline: "great sadness", 166 | }, 167 | { 168 | giveErrors: []error{ 169 | errors.New("foo"), 170 | newMultiErr( 171 | errors.New("bar"), 172 | ), 173 | }, 174 | wantError: newMultiErr( 175 | errors.New("foo"), 176 | errors.New("bar"), 177 | ), 178 | wantMultiline: "the following errors occurred:\n" + 179 | " - foo\n" + 180 | " - bar", 181 | wantSingleline: "foo; bar", 182 | }, 183 | { 184 | giveErrors: []error{errors.New("great sadness")}, 185 | wantError: errors.New("great sadness"), 186 | wantMultiline: "great sadness", 187 | wantSingleline: "great sadness", 188 | }, 189 | { 190 | giveErrors: []error{ 191 | errors.New("foo"), 192 | errors.New("bar"), 193 | }, 194 | wantError: newMultiErr( 195 | errors.New("foo"), 196 | errors.New("bar"), 197 | ), 198 | wantMultiline: "the following errors occurred:\n" + 199 | " - foo\n" + 200 | " - bar", 201 | wantSingleline: "foo; bar", 202 | }, 203 | { 204 | giveErrors: []error{ 205 | errors.New("great sadness"), 206 | errors.New("multi\n line\nerror message"), 207 | errors.New("single line error message"), 208 | }, 209 | wantError: newMultiErr( 210 | errors.New("great sadness"), 211 | errors.New("multi\n line\nerror message"), 212 | errors.New("single line error message"), 213 | ), 214 | wantMultiline: "the following errors occurred:\n" + 215 | " - great sadness\n" + 216 | " - multi\n" + 217 | " line\n" + 218 | " error message\n" + 219 | " - single line error message", 220 | wantSingleline: "great sadness; " + 221 | "multi\n line\nerror message; " + 222 | "single line error message", 223 | }, 224 | { 225 | giveErrors: []error{ 226 | errors.New("foo"), 227 | newMultiErr( 228 | errors.New("bar"), 229 | errors.New("baz"), 230 | ), 231 | errors.New("qux"), 232 | }, 233 | wantError: newMultiErr( 234 | errors.New("foo"), 235 | errors.New("bar"), 236 | errors.New("baz"), 237 | errors.New("qux"), 238 | ), 239 | wantMultiline: "the following errors occurred:\n" + 240 | " - foo\n" + 241 | " - bar\n" + 242 | " - baz\n" + 243 | " - qux", 244 | wantSingleline: "foo; bar; baz; qux", 245 | }, 246 | { 247 | giveErrors: []error{ 248 | errors.New("foo"), 249 | nil, 250 | newMultiErr( 251 | errors.New("bar"), 252 | ), 253 | nil, 254 | }, 255 | wantError: newMultiErr( 256 | errors.New("foo"), 257 | errors.New("bar"), 258 | ), 259 | wantMultiline: "the following errors occurred:\n" + 260 | " - foo\n" + 261 | " - bar", 262 | wantSingleline: "foo; bar", 263 | }, 264 | { 265 | giveErrors: []error{ 266 | errors.New("foo"), 267 | newMultiErr( 268 | errors.New("bar"), 269 | ), 270 | }, 271 | wantError: newMultiErr( 272 | errors.New("foo"), 273 | errors.New("bar"), 274 | ), 275 | wantMultiline: "the following errors occurred:\n" + 276 | " - foo\n" + 277 | " - bar", 278 | wantSingleline: "foo; bar", 279 | }, 280 | { 281 | giveErrors: []error{ 282 | errors.New("foo"), 283 | richFormatError{}, 284 | errors.New("bar"), 285 | }, 286 | wantError: newMultiErr( 287 | errors.New("foo"), 288 | richFormatError{}, 289 | errors.New("bar"), 290 | ), 291 | wantMultiline: "the following errors occurred:\n" + 292 | " - foo\n" + 293 | " - multiline\n" + 294 | " message\n" + 295 | " with plus\n" + 296 | " - bar", 297 | wantSingleline: "foo; without plus; bar", 298 | }, 299 | } 300 | 301 | for i, tt := range tests { 302 | t.Run(fmt.Sprint(i), func(t *testing.T) { 303 | err := Combine(tt.giveErrors...) 304 | require.Equal(t, tt.wantError, err) 305 | 306 | if tt.wantMultiline != "" { 307 | t.Run("Sprintf/multiline", func(t *testing.T) { 308 | assert.Equal(t, tt.wantMultiline, fmt.Sprintf("%+v", err)) 309 | }) 310 | } 311 | 312 | if tt.wantSingleline != "" { 313 | t.Run("Sprintf/singleline", func(t *testing.T) { 314 | assert.Equal(t, tt.wantSingleline, fmt.Sprintf("%v", err)) 315 | }) 316 | 317 | t.Run("Error()", func(t *testing.T) { 318 | assert.Equal(t, tt.wantSingleline, err.Error()) 319 | }) 320 | 321 | if s, ok := err.(fmt.Stringer); ok { 322 | t.Run("String()", func(t *testing.T) { 323 | assert.Equal(t, tt.wantSingleline, s.String()) 324 | }) 325 | } 326 | } 327 | }) 328 | } 329 | } 330 | 331 | func TestCombineDoesNotModifySlice(t *testing.T) { 332 | errors := []error{ 333 | errors.New("foo"), 334 | nil, 335 | errors.New("bar"), 336 | } 337 | 338 | assert.NotNil(t, Combine(errors...)) 339 | assert.Len(t, errors, 3) 340 | assert.Nil(t, errors[1], 3) 341 | } 342 | 343 | func TestCombineGoodCaseNoAlloc(t *testing.T) { 344 | errs := make([]error, 10) 345 | allocs := testing.AllocsPerRun(100, func() { 346 | Combine(errs...) 347 | }) 348 | assert.Equal(t, 0.0, allocs) 349 | } 350 | 351 | func TestAppend(t *testing.T) { 352 | tests := []struct { 353 | left error 354 | right error 355 | want error 356 | }{ 357 | { 358 | left: nil, 359 | right: nil, 360 | want: nil, 361 | }, 362 | { 363 | left: nil, 364 | right: errors.New("great sadness"), 365 | want: errors.New("great sadness"), 366 | }, 367 | { 368 | left: errors.New("great sadness"), 369 | right: nil, 370 | want: errors.New("great sadness"), 371 | }, 372 | { 373 | left: errors.New("foo"), 374 | right: errors.New("bar"), 375 | want: newMultiErr( 376 | errors.New("foo"), 377 | errors.New("bar"), 378 | ), 379 | }, 380 | { 381 | left: newMultiErr( 382 | errors.New("foo"), 383 | errors.New("bar"), 384 | ), 385 | right: errors.New("baz"), 386 | want: newMultiErr( 387 | errors.New("foo"), 388 | errors.New("bar"), 389 | errors.New("baz"), 390 | ), 391 | }, 392 | { 393 | left: errors.New("baz"), 394 | right: newMultiErr( 395 | errors.New("foo"), 396 | errors.New("bar"), 397 | ), 398 | want: newMultiErr( 399 | errors.New("baz"), 400 | errors.New("foo"), 401 | errors.New("bar"), 402 | ), 403 | }, 404 | { 405 | left: newMultiErr( 406 | errors.New("foo"), 407 | ), 408 | right: newMultiErr( 409 | errors.New("bar"), 410 | ), 411 | want: newMultiErr( 412 | errors.New("foo"), 413 | errors.New("bar"), 414 | ), 415 | }, 416 | } 417 | 418 | for _, tt := range tests { 419 | assert.Equal(t, tt.want, Append(tt.left, tt.right)) 420 | } 421 | } 422 | 423 | type notMultiErr struct{} 424 | 425 | var _ errorGroup = notMultiErr{} 426 | 427 | func (notMultiErr) Error() string { 428 | return "great sadness" 429 | } 430 | 431 | func (notMultiErr) Errors() []error { 432 | return []error{errors.New("great sadness")} 433 | } 434 | 435 | func TestErrors(t *testing.T) { 436 | tests := []struct { 437 | give error 438 | want []error 439 | 440 | // Don't attempt to cast to errorGroup or *multiError 441 | dontCast bool 442 | }{ 443 | {dontCast: true}, // nil 444 | { 445 | give: errors.New("hi"), 446 | want: []error{errors.New("hi")}, 447 | dontCast: true, 448 | }, 449 | { 450 | // We don't yet support non-multierr errors that do 451 | // not implement Unwrap() []error. 452 | give: notMultiErr{}, 453 | want: []error{notMultiErr{}}, 454 | dontCast: true, 455 | }, 456 | { 457 | give: Combine( 458 | errors.New("foo"), 459 | errors.New("bar"), 460 | ), 461 | want: []error{ 462 | errors.New("foo"), 463 | errors.New("bar"), 464 | }, 465 | }, 466 | { 467 | give: Append( 468 | errors.New("foo"), 469 | errors.New("bar"), 470 | ), 471 | want: []error{ 472 | errors.New("foo"), 473 | errors.New("bar"), 474 | }, 475 | }, 476 | { 477 | give: Append( 478 | errors.New("foo"), 479 | Combine( 480 | errors.New("bar"), 481 | ), 482 | ), 483 | want: []error{ 484 | errors.New("foo"), 485 | errors.New("bar"), 486 | }, 487 | }, 488 | { 489 | give: Combine( 490 | errors.New("foo"), 491 | Append( 492 | errors.New("bar"), 493 | errors.New("baz"), 494 | ), 495 | errors.New("qux"), 496 | ), 497 | want: []error{ 498 | errors.New("foo"), 499 | errors.New("bar"), 500 | errors.New("baz"), 501 | errors.New("qux"), 502 | }, 503 | }, 504 | } 505 | 506 | for i, tt := range tests { 507 | t.Run(fmt.Sprint(i), func(t *testing.T) { 508 | t.Run("Errors()", func(t *testing.T) { 509 | require.Equal(t, tt.want, Errors(tt.give)) 510 | }) 511 | 512 | if tt.dontCast { 513 | return 514 | } 515 | 516 | t.Run("multiError", func(t *testing.T) { 517 | require.Equal(t, tt.want, tt.give.(*multiError).Errors()) 518 | }) 519 | 520 | t.Run("errorGroup", func(t *testing.T) { 521 | require.Equal(t, tt.want, tt.give.(errorGroup).Errors()) 522 | }) 523 | }) 524 | } 525 | } 526 | 527 | func createMultiErrWithCapacity() error { 528 | // Create a multiError that has capacity for more errors so Append will 529 | // modify the underlying array that may be shared. 530 | return appendN(nil, errors.New("append"), 50) 531 | } 532 | 533 | func TestAppendDoesNotModify(t *testing.T) { 534 | initial := createMultiErrWithCapacity() 535 | err1 := Append(initial, errors.New("err1")) 536 | err2 := Append(initial, errors.New("err2")) 537 | 538 | // Make sure the error messages match, since we do modify the copyNeeded 539 | // atomic, the values cannot be compared. 540 | assert.EqualError(t, initial, createMultiErrWithCapacity().Error(), "Initial should not be modified") 541 | 542 | assert.EqualError(t, err1, Append(createMultiErrWithCapacity(), errors.New("err1")).Error()) 543 | assert.EqualError(t, err2, Append(createMultiErrWithCapacity(), errors.New("err2")).Error()) 544 | } 545 | 546 | func TestAppendRace(t *testing.T) { 547 | initial := createMultiErrWithCapacity() 548 | 549 | var wg sync.WaitGroup 550 | for i := 0; i < 10; i++ { 551 | wg.Add(1) 552 | go func() { 553 | defer wg.Done() 554 | 555 | err := initial 556 | for j := 0; j < 10; j++ { 557 | err = Append(err, errors.New("err")) 558 | } 559 | }() 560 | } 561 | 562 | wg.Wait() 563 | } 564 | 565 | func TestErrorsSliceIsImmutable(t *testing.T) { 566 | err1 := errors.New("err1") 567 | err2 := errors.New("err2") 568 | 569 | err := Append(err1, err2) 570 | gotErrors := Errors(err) 571 | require.Equal(t, []error{err1, err2}, gotErrors, "errors must match") 572 | 573 | gotErrors[0] = nil 574 | gotErrors[1] = errors.New("err3") 575 | 576 | require.Equal(t, []error{err1, err2}, Errors(err), 577 | "errors must match after modification") 578 | } 579 | 580 | func TestNilMultierror(t *testing.T) { 581 | // For safety, all operations on multiError should be safe even if it is 582 | // nil. 583 | var err *multiError 584 | 585 | require.Empty(t, err.Error()) 586 | require.Empty(t, err.Errors()) 587 | } 588 | 589 | func TestAppendInto(t *testing.T) { 590 | tests := []struct { 591 | desc string 592 | into *error 593 | give error 594 | want error 595 | }{ 596 | { 597 | desc: "append into empty", 598 | into: new(error), 599 | give: errors.New("foo"), 600 | want: errors.New("foo"), 601 | }, 602 | { 603 | desc: "append into non-empty, non-multierr", 604 | into: errorPtr(errors.New("foo")), 605 | give: errors.New("bar"), 606 | want: Combine( 607 | errors.New("foo"), 608 | errors.New("bar"), 609 | ), 610 | }, 611 | { 612 | desc: "append into non-empty multierr", 613 | into: errorPtr(Combine( 614 | errors.New("foo"), 615 | errors.New("bar"), 616 | )), 617 | give: errors.New("baz"), 618 | want: Combine( 619 | errors.New("foo"), 620 | errors.New("bar"), 621 | errors.New("baz"), 622 | ), 623 | }, 624 | } 625 | 626 | for _, tt := range tests { 627 | t.Run(tt.desc, func(t *testing.T) { 628 | assert.True(t, AppendInto(tt.into, tt.give)) 629 | assert.Equal(t, tt.want, *tt.into) 630 | }) 631 | } 632 | } 633 | 634 | func TestAppendInvoke(t *testing.T) { 635 | tests := []struct { 636 | desc string 637 | into *error 638 | give Invoker 639 | want error 640 | }{ 641 | { 642 | desc: "append into empty", 643 | into: new(error), 644 | give: Invoke(func() error { 645 | return errors.New("foo") 646 | }), 647 | want: errors.New("foo"), 648 | }, 649 | { 650 | desc: "append into non-empty, non-multierr", 651 | into: errorPtr(errors.New("foo")), 652 | give: Invoke(func() error { 653 | return errors.New("bar") 654 | }), 655 | want: Combine( 656 | errors.New("foo"), 657 | errors.New("bar"), 658 | ), 659 | }, 660 | { 661 | desc: "append into non-empty multierr", 662 | into: errorPtr(Combine( 663 | errors.New("foo"), 664 | errors.New("bar"), 665 | )), 666 | give: Invoke(func() error { 667 | return errors.New("baz") 668 | }), 669 | want: Combine( 670 | errors.New("foo"), 671 | errors.New("bar"), 672 | errors.New("baz"), 673 | ), 674 | }, 675 | { 676 | desc: "close/empty", 677 | into: new(error), 678 | give: Close(newCloserMock(t, errors.New("foo"))), 679 | want: errors.New("foo"), 680 | }, 681 | { 682 | desc: "close/no fail", 683 | into: new(error), 684 | give: Close(newCloserMock(t, nil)), 685 | want: nil, 686 | }, 687 | { 688 | desc: "close/non-empty", 689 | into: errorPtr(errors.New("foo")), 690 | give: Close(newCloserMock(t, errors.New("bar"))), 691 | want: Combine( 692 | errors.New("foo"), 693 | errors.New("bar"), 694 | ), 695 | }, 696 | } 697 | for _, tt := range tests { 698 | t.Run(tt.desc, func(t *testing.T) { 699 | AppendInvoke(tt.into, tt.give) 700 | assert.Equal(t, tt.want, *tt.into) 701 | }) 702 | } 703 | } 704 | 705 | func TestClose(t *testing.T) { 706 | t.Run("fail", func(t *testing.T) { 707 | give := errors.New("great sadness") 708 | got := Close(newCloserMock(t, give)).Invoke() 709 | assert.Same(t, give, got) 710 | }) 711 | 712 | t.Run("success", func(t *testing.T) { 713 | got := Close(newCloserMock(t, nil)).Invoke() 714 | assert.Nil(t, got) 715 | }) 716 | } 717 | 718 | func TestAppendIntoNil(t *testing.T) { 719 | t.Run("nil pointer panics", func(t *testing.T) { 720 | assert.Panics(t, func() { 721 | AppendInto(nil, errors.New("foo")) 722 | }) 723 | }) 724 | 725 | t.Run("nil error is no-op", func(t *testing.T) { 726 | t.Run("empty left", func(t *testing.T) { 727 | var err error 728 | assert.False(t, AppendInto(&err, nil)) 729 | assert.Nil(t, err) 730 | }) 731 | 732 | t.Run("non-empty left", func(t *testing.T) { 733 | err := errors.New("foo") 734 | assert.False(t, AppendInto(&err, nil)) 735 | assert.Equal(t, errors.New("foo"), err) 736 | }) 737 | }) 738 | } 739 | 740 | func TestAppendFunc(t *testing.T) { 741 | var ( 742 | errDeferred = errors.New("deferred func called") 743 | errOriginal = errors.New("original error") 744 | ) 745 | 746 | stopFunc := func() error { 747 | return errDeferred 748 | } 749 | 750 | err := func() (err error) { 751 | defer AppendFunc(&err, stopFunc) 752 | 753 | return errOriginal 754 | }() 755 | assert.Equal(t, []error{errOriginal, errDeferred}, Errors(err), "both deferred and original error must be returned") 756 | } 757 | 758 | func errorPtr(err error) *error { 759 | return &err 760 | } 761 | 762 | type closerMock func() error 763 | 764 | func (c closerMock) Close() error { 765 | return c() 766 | } 767 | 768 | func newCloserMock(tb testing.TB, err error) io.Closer { 769 | var closed bool 770 | tb.Cleanup(func() { 771 | if !closed { 772 | tb.Error("closerMock wasn't closed before test end") 773 | } 774 | }) 775 | return closerMock(func() error { 776 | closed = true 777 | return err 778 | }) 779 | } 780 | 781 | func TestErrorsOnErrorsJoin(t *testing.T) { 782 | err1 := errors.New("err1") 783 | err2 := errors.New("err2") 784 | err := errors.Join(err1, err2) 785 | 786 | errs := Errors(err) 787 | assert.Equal(t, 2, len(errs)) 788 | assert.Equal(t, err1, errs[0]) 789 | assert.Equal(t, err2, errs[1]) 790 | } 791 | 792 | func TestEveryWithErrorsJoin(t *testing.T) { 793 | myError1 := errors.New("woeful misfortune") 794 | myError2 := errors.New("worrisome travesty") 795 | 796 | t.Run("all match", func(t *testing.T) { 797 | err := errors.Join(myError1, myError1, myError1) 798 | 799 | assert.True(t, errors.Is(err, myError1)) 800 | assert.True(t, Every(err, myError1)) 801 | assert.False(t, errors.Is(err, myError2)) 802 | assert.False(t, Every(err, myError2)) 803 | }) 804 | 805 | t.Run("one matches", func(t *testing.T) { 806 | err := errors.Join(myError1, myError2) 807 | 808 | assert.True(t, errors.Is(err, myError1)) 809 | assert.False(t, Every(err, myError1)) 810 | assert.True(t, errors.Is(err, myError2)) 811 | assert.False(t, Every(err, myError2)) 812 | }) 813 | } 814 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package multierr_test 22 | 23 | import ( 24 | "errors" 25 | "fmt" 26 | "io" 27 | 28 | "go.uber.org/multierr" 29 | ) 30 | 31 | func ExampleCombine() { 32 | err := multierr.Combine( 33 | errors.New("call 1 failed"), 34 | nil, // successful request 35 | errors.New("call 3 failed"), 36 | nil, // successful request 37 | errors.New("call 5 failed"), 38 | ) 39 | fmt.Printf("%+v", err) 40 | // Output: 41 | // the following errors occurred: 42 | // - call 1 failed 43 | // - call 3 failed 44 | // - call 5 failed 45 | } 46 | 47 | func ExampleAppend() { 48 | var err error 49 | err = multierr.Append(err, errors.New("call 1 failed")) 50 | err = multierr.Append(err, errors.New("call 2 failed")) 51 | fmt.Println(err) 52 | // Output: 53 | // call 1 failed; call 2 failed 54 | } 55 | 56 | func ExampleErrors() { 57 | err := multierr.Combine( 58 | nil, // successful request 59 | errors.New("call 2 failed"), 60 | errors.New("call 3 failed"), 61 | ) 62 | err = multierr.Append(err, nil) // successful request 63 | err = multierr.Append(err, errors.New("call 5 failed")) 64 | 65 | errors := multierr.Errors(err) 66 | for _, err := range errors { 67 | fmt.Println(err) 68 | } 69 | // Output: 70 | // call 2 failed 71 | // call 3 failed 72 | // call 5 failed 73 | } 74 | 75 | func ExampleAppendInto() { 76 | var err error 77 | 78 | if multierr.AppendInto(&err, errors.New("foo")) { 79 | fmt.Println("call 1 failed") 80 | } 81 | 82 | if multierr.AppendInto(&err, nil) { 83 | fmt.Println("call 2 failed") 84 | } 85 | 86 | if multierr.AppendInto(&err, errors.New("baz")) { 87 | fmt.Println("call 3 failed") 88 | } 89 | 90 | fmt.Println(err) 91 | // Output: 92 | // call 1 failed 93 | // call 3 failed 94 | // foo; baz 95 | } 96 | 97 | type fakeCloser func() error 98 | 99 | func (f fakeCloser) Close() error { 100 | return f() 101 | } 102 | 103 | func FakeCloser(err error) io.Closer { 104 | return fakeCloser(func() error { 105 | return err 106 | }) 107 | } 108 | 109 | func ExampleClose() { 110 | var err error 111 | 112 | closer := FakeCloser(errors.New("foo")) 113 | 114 | defer func() { 115 | fmt.Println(err) 116 | }() 117 | defer multierr.AppendInvoke(&err, multierr.Close(closer)) 118 | 119 | fmt.Println("Hello, World") 120 | 121 | // Output: 122 | // Hello, World 123 | // foo 124 | } 125 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go.uber.org/multierr 2 | 3 | go 1.20 4 | 5 | require github.com/stretchr/testify v1.7.0 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 8 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 12 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 13 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 14 | -------------------------------------------------------------------------------- /tools/go.mod: -------------------------------------------------------------------------------- 1 | module go.uber.org/multierr/tools 2 | 3 | go 1.20 4 | 5 | require ( 6 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 7 | honnef.co/go/tools v0.4.0 8 | ) 9 | 10 | require ( 11 | github.com/BurntSushi/toml v1.2.1 // indirect 12 | golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a // indirect 13 | golang.org/x/mod v0.7.0 // indirect 14 | golang.org/x/sys v0.4.0 // indirect 15 | golang.org/x/tools v0.5.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /tools/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= 2 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 4 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 5 | golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a h1:Jw5wfR+h9mnIYH+OtGT2im5wV1YGGDora5vTv/aa5bE= 6 | golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= 7 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= 8 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 9 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 10 | golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= 11 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 12 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 13 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 14 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 15 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 16 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 17 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 18 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 19 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 20 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 21 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 22 | golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4= 23 | golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= 24 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 25 | honnef.co/go/tools v0.4.0 h1:lyXVV1c8wUBJRKqI8JgIpT8TW1VDagfYYaxbKa/HoL8= 26 | honnef.co/go/tools v0.4.0/go.mod h1:36ZgoUOrqOk1GxwHhyryEkq8FQWkUO2xGuSMhUCcdvA= 27 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | //go:build tools 22 | // +build tools 23 | 24 | package multierr 25 | 26 | import ( 27 | // Tools we use during development. 28 | _ "golang.org/x/lint/golint" 29 | _ "honnef.co/go/tools/cmd/staticcheck" 30 | ) 31 | --------------------------------------------------------------------------------