├── .github ├── dependabot.yml └── workflows │ ├── actionlint.yml │ └── go-multierror.yml ├── .go-version ├── CODEOWNERS ├── LICENSE ├── Makefile ├── README.md ├── append.go ├── append_test.go ├── flatten.go ├── flatten_test.go ├── format.go ├── format_test.go ├── go.mod ├── go.sum ├── group.go ├── group_test.go ├── multierror.go ├── multierror_test.go ├── prefix.go ├── prefix_test.go ├── sort.go └── sort_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/actionlint.yml: -------------------------------------------------------------------------------- 1 | name: Lint GitHub Actions Workflows 2 | 3 | on: 4 | push: 5 | paths: 6 | - .github/** 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | actionlint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 16 | - name: "Check workflow files" 17 | uses: docker://docker.mirror.hashicorp.services/rhysd/actionlint:latest 18 | with: 19 | args: -color 20 | -------------------------------------------------------------------------------- /.github/workflows/go-multierror.yml: -------------------------------------------------------------------------------- 1 | name: hashicorp/go-multierror/go-multierror 2 | on: 3 | - push 4 | - pull_request 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | go-fmt: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Get go-version 14 | run: go version 15 | - name: Checkout code 16 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 17 | - name: Setup go 18 | uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 19 | with: 20 | go-version-file: go.mod 21 | - name: check go fmt 22 | run: |- 23 | files="$(go fmt ./...)" 24 | if [ -n "$files" ]; then 25 | echo "The following file(s) do not conform to go fmt:" 26 | echo "$files" 27 | exit 1 28 | fi 29 | 30 | lint: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout code 34 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 35 | - name: Setup go 36 | uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 37 | with: 38 | go-version-file: go.mod 39 | - name: Run golangci-lint 40 | uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5 41 | 42 | linux-tests: 43 | runs-on: ubuntu-latest 44 | env: 45 | TEST_RESULTS_PATH: '/tmp/test-results' 46 | strategy: 47 | matrix: 48 | go-version: 49 | - '1.13' # oldest supported; named in go.mod 50 | - 'oldstable' 51 | - 'stable' 52 | steps: 53 | - name: Checkout code 54 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 55 | - name: Setup go 56 | uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 57 | with: 58 | go-version: ${{ matrix.go-version }} 59 | - name: Install gotestsum 60 | uses: autero1/action-gotestsum@7263b9d73912eec65f46337689e59fac865c425f # v2.0.0 61 | with: 62 | gotestsum_version: 1.9.0 63 | - name: Get go version and env 64 | run: | 65 | go version 66 | go env 67 | - name: Create test directory 68 | run: mkdir -p "$TEST_RESULTS_PATH/go-multierror" 69 | - name: Run go tests 70 | env: 71 | PLATFORM: linux 72 | REPORT_FILE: ${{ env.TEST_RESULTS_PATH }}/go-multierror/gotestsum-report.xml 73 | run: |- 74 | gotestsum --format=short-verbose --junitfile ${{ env.REPORT_FILE }} -- -p 2 -cover -coverprofile=coverage-linux.out ./... 75 | - name: Upload and save artifacts 76 | uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 77 | with: 78 | path: ${{ env.TEST_RESULTS_PATH }} 79 | name: tests-linux-${{ matrix.go-version }} 80 | - name: Upload coverage report 81 | uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 82 | with: 83 | path: coverage-linux.out 84 | name: Coverage-report-linux-${{matrix.go-version}} 85 | - name: Display coverage report 86 | run: go tool cover -func=coverage-linux.out 87 | 88 | windows-tests: 89 | runs-on: windows-latest 90 | env: 91 | TEST_RESULTS_PATH: 'c:\Users\runneradmin\AppData\Local\Temp\test-results' 92 | strategy: 93 | matrix: 94 | go-version: 95 | - '1.13' # oldest supported; named in go.mod 96 | - 'oldstable' 97 | - 'stable' 98 | steps: 99 | - name: Checkout code 100 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 101 | - name: Setup Go 102 | uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 103 | with: 104 | go-version: ${{ matrix.go-version }} 105 | - name: Get go-version and env 106 | run: | 107 | go version 108 | go env 109 | - run: git config --global core.autocrlf false 110 | - name: Download go modules 111 | run: go mod download 112 | - name: Install gotestsum 113 | uses: autero1/action-gotestsum@7263b9d73912eec65f46337689e59fac865c425f # v2.0.0 114 | with: 115 | gotestsum_version: 1.9.0 116 | - name: Run go tests 117 | env: 118 | PLATFORM: win 119 | REPORT_FILE: ${{ env.TEST_RESULTS_PATH }}/go-multierror/gotestsum-report.xml 120 | run: |- 121 | gotestsum.exe --format=short-verbose --junitfile ${{ env.REPORT_FILE }} -- -p 2 -cover -coverprofile="coverage-win.out" ./... 122 | - name: Upload and save artifacts 123 | uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 124 | with: 125 | path: ${{ env.TEST_RESULTS_PATH }} 126 | name: tests-windows-${{ matrix.go-version }} 127 | - name: Upload coverage test 128 | uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 129 | with: 130 | path: coverage-win.out 131 | name: Coverage-report-win-${{matrix.go-version}} 132 | - name: Display coverage report 133 | run: go tool cover -func=coverage-win.out 134 | shell: cmd 135 | -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.13 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. 2 | # More on CODEOWNERS files: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 3 | 4 | # Default owner 5 | * @hashicorp/team-ip-compliance 6 | 7 | # Add override rules below. Each line is a file/folder pattern followed by one or more owners. 8 | # Being an owner means those groups or individuals will be added as reviewers to PRs affecting 9 | # those areas of the code. 10 | # Examples: 11 | # /docs/ @docs-team 12 | # *.js @js-team 13 | # *.go @go-team -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 HashiCorp, Inc. 2 | 3 | Mozilla Public License, version 2.0 4 | 5 | 1. Definitions 6 | 7 | 1.1. “Contributor” 8 | 9 | means each individual or legal entity that creates, contributes to the 10 | creation of, or owns Covered Software. 11 | 12 | 1.2. “Contributor Version” 13 | 14 | means the combination of the Contributions of others (if any) used by a 15 | Contributor and that particular Contributor’s Contribution. 16 | 17 | 1.3. “Contribution” 18 | 19 | means Covered Software of a particular Contributor. 20 | 21 | 1.4. “Covered Software” 22 | 23 | means Source Code Form to which the initial Contributor has attached the 24 | notice in Exhibit A, the Executable Form of such Source Code Form, and 25 | Modifications of such Source Code Form, in each case including portions 26 | thereof. 27 | 28 | 1.5. “Incompatible With Secondary Licenses” 29 | means 30 | 31 | a. that the initial Contributor has attached the notice described in 32 | Exhibit B to the Covered Software; or 33 | 34 | b. that the Covered Software was made available under the terms of version 35 | 1.1 or earlier of the License, but not also under the terms of a 36 | Secondary License. 37 | 38 | 1.6. “Executable Form” 39 | 40 | means any form of the work other than Source Code Form. 41 | 42 | 1.7. “Larger Work” 43 | 44 | means a work that combines Covered Software with other material, in a separate 45 | file or files, that is not Covered Software. 46 | 47 | 1.8. “License” 48 | 49 | means this document. 50 | 51 | 1.9. “Licensable” 52 | 53 | means having the right to grant, to the maximum extent possible, whether at the 54 | time of the initial grant or subsequently, any and all of the rights conveyed by 55 | this License. 56 | 57 | 1.10. “Modifications” 58 | 59 | means any of the following: 60 | 61 | a. any file in Source Code Form that results from an addition to, deletion 62 | from, or modification of the contents of Covered Software; or 63 | 64 | b. any new file in Source Code Form that contains any Covered Software. 65 | 66 | 1.11. “Patent Claims” of a Contributor 67 | 68 | means any patent claim(s), including without limitation, method, process, 69 | and apparatus claims, in any patent Licensable by such Contributor that 70 | would be infringed, but for the grant of the License, by the making, 71 | using, selling, offering for sale, having made, import, or transfer of 72 | either its Contributions or its Contributor Version. 73 | 74 | 1.12. “Secondary License” 75 | 76 | means either the GNU General Public License, Version 2.0, the GNU Lesser 77 | General Public License, Version 2.1, the GNU Affero General Public 78 | License, Version 3.0, or any later versions of those licenses. 79 | 80 | 1.13. “Source Code Form” 81 | 82 | means the form of the work preferred for making modifications. 83 | 84 | 1.14. “You” (or “Your”) 85 | 86 | means an individual or a legal entity exercising rights under this 87 | License. For legal entities, “You” includes any entity that controls, is 88 | controlled by, or is under common control with You. For purposes of this 89 | definition, “control” means (a) the power, direct or indirect, to cause 90 | the direction or management of such entity, whether by contract or 91 | otherwise, or (b) ownership of more than fifty percent (50%) of the 92 | outstanding shares or beneficial ownership of such entity. 93 | 94 | 95 | 2. License Grants and Conditions 96 | 97 | 2.1. Grants 98 | 99 | Each Contributor hereby grants You a world-wide, royalty-free, 100 | non-exclusive license: 101 | 102 | a. under intellectual property rights (other than patent or trademark) 103 | Licensable by such Contributor to use, reproduce, make available, 104 | modify, display, perform, distribute, and otherwise exploit its 105 | Contributions, either on an unmodified basis, with Modifications, or as 106 | part of a Larger Work; and 107 | 108 | b. under Patent Claims of such Contributor to make, use, sell, offer for 109 | sale, have made, import, and otherwise transfer either its Contributions 110 | or its Contributor Version. 111 | 112 | 2.2. Effective Date 113 | 114 | The licenses granted in Section 2.1 with respect to any Contribution become 115 | effective for each Contribution on the date the Contributor first distributes 116 | such Contribution. 117 | 118 | 2.3. Limitations on Grant Scope 119 | 120 | The licenses granted in this Section 2 are the only rights granted under this 121 | License. No additional rights or licenses will be implied from the distribution 122 | or licensing of Covered Software under this License. Notwithstanding Section 123 | 2.1(b) above, no patent license is granted by a Contributor: 124 | 125 | a. for any code that a Contributor has removed from Covered Software; or 126 | 127 | b. for infringements caused by: (i) Your and any other third party’s 128 | modifications of Covered Software, or (ii) the combination of its 129 | Contributions with other software (except as part of its Contributor 130 | Version); or 131 | 132 | c. under Patent Claims infringed by Covered Software in the absence of its 133 | Contributions. 134 | 135 | This License does not grant any rights in the trademarks, service marks, or 136 | logos of any Contributor (except as may be necessary to comply with the 137 | notice requirements in Section 3.4). 138 | 139 | 2.4. Subsequent Licenses 140 | 141 | No Contributor makes additional grants as a result of Your choice to 142 | distribute the Covered Software under a subsequent version of this License 143 | (see Section 10.2) or under the terms of a Secondary License (if permitted 144 | under the terms of Section 3.3). 145 | 146 | 2.5. Representation 147 | 148 | Each Contributor represents that the Contributor believes its Contributions 149 | are its original creation(s) or it has sufficient rights to grant the 150 | rights to its Contributions conveyed by this License. 151 | 152 | 2.6. Fair Use 153 | 154 | This License is not intended to limit any rights You have under applicable 155 | copyright doctrines of fair use, fair dealing, or other equivalents. 156 | 157 | 2.7. Conditions 158 | 159 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 160 | Section 2.1. 161 | 162 | 163 | 3. Responsibilities 164 | 165 | 3.1. Distribution of Source Form 166 | 167 | All distribution of Covered Software in Source Code Form, including any 168 | Modifications that You create or to which You contribute, must be under the 169 | terms of this License. You must inform recipients that the Source Code Form 170 | of the Covered Software is governed by the terms of this License, and how 171 | they can obtain a copy of this License. You may not attempt to alter or 172 | restrict the recipients’ rights in the Source Code Form. 173 | 174 | 3.2. Distribution of Executable Form 175 | 176 | If You distribute Covered Software in Executable Form then: 177 | 178 | a. such Covered Software must also be made available in Source Code Form, 179 | as described in Section 3.1, and You must inform recipients of the 180 | Executable Form how they can obtain a copy of such Source Code Form by 181 | reasonable means in a timely manner, at a charge no more than the cost 182 | of distribution to the recipient; and 183 | 184 | b. You may distribute such Executable Form under the terms of this License, 185 | or sublicense it under different terms, provided that the license for 186 | the Executable Form does not attempt to limit or alter the recipients’ 187 | rights in the Source Code Form under this License. 188 | 189 | 3.3. Distribution of a Larger Work 190 | 191 | You may create and distribute a Larger Work under terms of Your choice, 192 | provided that You also comply with the requirements of this License for the 193 | Covered Software. If the Larger Work is a combination of Covered Software 194 | with a work governed by one or more Secondary Licenses, and the Covered 195 | Software is not Incompatible With Secondary Licenses, this License permits 196 | You to additionally distribute such Covered Software under the terms of 197 | such Secondary License(s), so that the recipient of the Larger Work may, at 198 | their option, further distribute the Covered Software under the terms of 199 | either this License or such Secondary License(s). 200 | 201 | 3.4. Notices 202 | 203 | You may not remove or alter the substance of any license notices (including 204 | copyright notices, patent notices, disclaimers of warranty, or limitations 205 | of liability) contained within the Source Code Form of the Covered 206 | Software, except that You may alter any license notices to the extent 207 | required to remedy known factual inaccuracies. 208 | 209 | 3.5. Application of Additional Terms 210 | 211 | You may choose to offer, and to charge a fee for, warranty, support, 212 | indemnity or liability obligations to one or more recipients of Covered 213 | Software. However, You may do so only on Your own behalf, and not on behalf 214 | of any Contributor. You must make it absolutely clear that any such 215 | warranty, support, indemnity, or liability obligation is offered by You 216 | alone, and You hereby agree to indemnify every Contributor for any 217 | liability incurred by such Contributor as a result of warranty, support, 218 | indemnity or liability terms You offer. You may include additional 219 | disclaimers of warranty and limitations of liability specific to any 220 | jurisdiction. 221 | 222 | 4. Inability to Comply Due to Statute or Regulation 223 | 224 | If it is impossible for You to comply with any of the terms of this License 225 | with respect to some or all of the Covered Software due to statute, judicial 226 | order, or regulation then You must: (a) comply with the terms of this License 227 | to the maximum extent possible; and (b) describe the limitations and the code 228 | they affect. Such description must be placed in a text file included with all 229 | distributions of the Covered Software under this License. Except to the 230 | extent prohibited by statute or regulation, such description must be 231 | sufficiently detailed for a recipient of ordinary skill to be able to 232 | understand it. 233 | 234 | 5. Termination 235 | 236 | 5.1. The rights granted under this License will terminate automatically if You 237 | fail to comply with any of its terms. However, if You become compliant, 238 | then the rights granted under this License from a particular Contributor 239 | are reinstated (a) provisionally, unless and until such Contributor 240 | explicitly and finally terminates Your grants, and (b) on an ongoing basis, 241 | if such Contributor fails to notify You of the non-compliance by some 242 | reasonable means prior to 60 days after You have come back into compliance. 243 | Moreover, Your grants from a particular Contributor are reinstated on an 244 | ongoing basis if such Contributor notifies You of the non-compliance by 245 | some reasonable means, this is the first time You have received notice of 246 | non-compliance with this License from such Contributor, and You become 247 | compliant prior to 30 days after Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, counter-claims, 251 | and cross-claims) alleging that a Contributor Version directly or 252 | indirectly infringes any patent, then the rights granted to You by any and 253 | all Contributors for the Covered Software under Section 2.1 of this License 254 | shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 257 | license agreements (excluding distributors and resellers) which have been 258 | validly granted by You or Your distributors under this License prior to 259 | termination shall survive termination. 260 | 261 | 6. Disclaimer of Warranty 262 | 263 | Covered Software is provided under this License on an “as is” basis, without 264 | warranty of any kind, either expressed, implied, or statutory, including, 265 | without limitation, warranties that the Covered Software is free of defects, 266 | merchantable, fit for a particular purpose or non-infringing. The entire 267 | risk as to the quality and performance of the Covered Software is with You. 268 | Should any Covered Software prove defective in any respect, You (not any 269 | Contributor) assume the cost of any necessary servicing, repair, or 270 | correction. This disclaimer of warranty constitutes an essential part of this 271 | License. No use of any Covered Software is authorized under this License 272 | except under this disclaimer. 273 | 274 | 7. Limitation of Liability 275 | 276 | Under no circumstances and under no legal theory, whether tort (including 277 | negligence), contract, or otherwise, shall any Contributor, or anyone who 278 | distributes Covered Software as permitted above, be liable to You for any 279 | direct, indirect, special, incidental, or consequential damages of any 280 | character including, without limitation, damages for lost profits, loss of 281 | goodwill, work stoppage, computer failure or malfunction, or any and all 282 | other commercial damages or losses, even if such party shall have been 283 | informed of the possibility of such damages. This limitation of liability 284 | shall not apply to liability for death or personal injury resulting from such 285 | party’s negligence to the extent applicable law prohibits such limitation. 286 | Some jurisdictions do not allow the exclusion or limitation of incidental or 287 | consequential damages, so this exclusion and limitation may not apply to You. 288 | 289 | 8. Litigation 290 | 291 | Any litigation relating to this License may be brought only in the courts of 292 | a jurisdiction where the defendant maintains its principal place of business 293 | and such litigation shall be governed by laws of that jurisdiction, without 294 | reference to its conflict-of-law provisions. Nothing in this Section shall 295 | prevent a party’s ability to bring cross-claims or counter-claims. 296 | 297 | 9. Miscellaneous 298 | 299 | This License represents the complete agreement concerning the subject matter 300 | hereof. If any provision of this License is held to be unenforceable, such 301 | provision shall be reformed only to the extent necessary to make it 302 | enforceable. Any law or regulation which provides that the language of a 303 | contract shall be construed against the drafter shall not be used to construe 304 | this License against a Contributor. 305 | 306 | 307 | 10. Versions of the License 308 | 309 | 10.1. New Versions 310 | 311 | Mozilla Foundation is the license steward. Except as provided in Section 312 | 10.3, no one other than the license steward has the right to modify or 313 | publish new versions of this License. Each version will be given a 314 | distinguishing version number. 315 | 316 | 10.2. Effect of New Versions 317 | 318 | You may distribute the Covered Software under the terms of the version of 319 | the License under which You originally received the Covered Software, or 320 | under the terms of any subsequent version published by the license 321 | steward. 322 | 323 | 10.3. Modified Versions 324 | 325 | If you create software not governed by this License, and you want to 326 | create a new license for such software, you may create and use a modified 327 | version of this License if you rename the license and remove any 328 | references to the name of the license steward (except to note that such 329 | modified license differs from this License). 330 | 331 | 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses 332 | If You choose to distribute Source Code Form that is Incompatible With 333 | Secondary Licenses under the terms of this version of the License, the 334 | notice described in Exhibit B of this License must be attached. 335 | 336 | Exhibit A - Source Code Form License Notice 337 | 338 | This Source Code Form is subject to the 339 | terms of the Mozilla Public License, v. 340 | 2.0. If a copy of the MPL was not 341 | distributed with this file, You can 342 | obtain one at 343 | http://mozilla.org/MPL/2.0/. 344 | 345 | If it is not possible or desirable to put the notice in a particular file, then 346 | You may include the notice in a location (such as a LICENSE file in a relevant 347 | directory) where a recipient would be likely to look for such a notice. 348 | 349 | You may add additional accurate notices of copyright ownership. 350 | 351 | Exhibit B - “Incompatible With Secondary Licenses” Notice 352 | 353 | This Source Code Form is “Incompatible 354 | With Secondary Licenses”, as defined by 355 | the Mozilla Public License, v. 2.0. 356 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TEST?=./... 2 | 3 | default: test 4 | 5 | # test runs the test suite and vets the code. 6 | test: generate 7 | @echo "==> Running tests..." 8 | @go list $(TEST) \ 9 | | grep -v "/vendor/" \ 10 | | xargs -n1 go test -timeout=60s -parallel=10 ${TESTARGS} 11 | 12 | # testrace runs the race checker 13 | testrace: generate 14 | @echo "==> Running tests (race)..." 15 | @go list $(TEST) \ 16 | | grep -v "/vendor/" \ 17 | | xargs -n1 go test -timeout=60s -race ${TESTARGS} 18 | 19 | # updatedeps installs all the dependencies needed to run and build. 20 | updatedeps: 21 | @sh -c "'${CURDIR}/scripts/deps.sh' '${NAME}'" 22 | 23 | # generate runs `go generate` to build the dynamically generated source files. 24 | generate: 25 | @echo "==> Generating..." 26 | @find . -type f -name '.DS_Store' -delete 27 | @go list ./... \ 28 | | grep -v "/vendor/" \ 29 | | xargs -n1 go generate 30 | 31 | .PHONY: default test testrace updatedeps generate 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-multierror 2 | 3 | [![CircleCI](https://img.shields.io/circleci/build/github/hashicorp/go-multierror/master)](https://circleci.com/gh/hashicorp/go-multierror) 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/hashicorp/go-multierror.svg)](https://pkg.go.dev/github.com/hashicorp/go-multierror) 5 | ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/hashicorp/go-multierror) 6 | 7 | [circleci]: https://app.circleci.com/pipelines/github/hashicorp/go-multierror 8 | [godocs]: https://pkg.go.dev/github.com/hashicorp/go-multierror 9 | 10 | `go-multierror` is a package for Go that provides a mechanism for 11 | representing a list of `error` values as a single `error`. 12 | 13 | This allows a function in Go to return an `error` that might actually 14 | be a list of errors. If the caller knows this, they can unwrap the 15 | list and access the errors. If the caller doesn't know, the error 16 | formats to a nice human-readable format. 17 | 18 | `go-multierror` is fully compatible with the Go standard library 19 | [errors](https://golang.org/pkg/errors/) package, including the 20 | functions `As`, `Is`, and `Unwrap`. This provides a standardized approach 21 | for introspecting on error values. 22 | 23 | ## Installation and Docs 24 | 25 | Install using `go get github.com/hashicorp/go-multierror`. 26 | 27 | Full documentation is available at 28 | https://pkg.go.dev/github.com/hashicorp/go-multierror 29 | 30 | ### Requires go version 1.13 or newer 31 | 32 | `go-multierror` requires go version 1.13 or newer. Go 1.13 introduced 33 | [error wrapping](https://golang.org/doc/go1.13#error_wrapping), which 34 | this library takes advantage of. 35 | 36 | If you need to use an earlier version of go, you can use the 37 | [v1.0.0](https://github.com/hashicorp/go-multierror/tree/v1.0.0) 38 | tag, which doesn't rely on features in go 1.13. 39 | 40 | If you see compile errors that look like the below, it's likely that 41 | you're on an older version of go: 42 | 43 | ``` 44 | /go/src/github.com/hashicorp/go-multierror/multierror.go:112:9: undefined: errors.As 45 | /go/src/github.com/hashicorp/go-multierror/multierror.go:117:9: undefined: errors.Is 46 | ``` 47 | 48 | ## Usage 49 | 50 | go-multierror is easy to use and purposely built to be unobtrusive in 51 | existing Go applications/libraries that may not be aware of it. 52 | 53 | **Building a list of errors** 54 | 55 | The `Append` function is used to create a list of errors. This function 56 | behaves a lot like the Go built-in `append` function: it doesn't matter 57 | if the first argument is nil, a `multierror.Error`, or any other `error`, 58 | the function behaves as you would expect. 59 | 60 | ```go 61 | var result error 62 | 63 | if err := step1(); err != nil { 64 | result = multierror.Append(result, err) 65 | } 66 | if err := step2(); err != nil { 67 | result = multierror.Append(result, err) 68 | } 69 | 70 | return result 71 | ``` 72 | 73 | **Customizing the formatting of the errors** 74 | 75 | By specifying a custom `ErrorFormat`, you can customize the format 76 | of the `Error() string` function: 77 | 78 | ```go 79 | var result *multierror.Error 80 | 81 | // ... accumulate errors here, maybe using Append 82 | 83 | if result != nil { 84 | result.ErrorFormat = func([]error) string { 85 | return "errors!" 86 | } 87 | } 88 | ``` 89 | 90 | **Accessing the list of errors** 91 | 92 | `multierror.Error` implements `error` so if the caller doesn't know about 93 | multierror, it will work just fine. But if you're aware a multierror might 94 | be returned, you can use type switches to access the list of errors: 95 | 96 | ```go 97 | if err := something(); err != nil { 98 | if merr, ok := err.(*multierror.Error); ok { 99 | // Use merr.Errors 100 | } 101 | } 102 | ``` 103 | 104 | You can also use the standard [`errors.Unwrap`](https://golang.org/pkg/errors/#Unwrap) 105 | function. This will continue to unwrap into subsequent errors until none exist. 106 | 107 | **Extracting an error** 108 | 109 | The standard library [`errors.As`](https://golang.org/pkg/errors/#As) 110 | function can be used directly with a multierror to extract a specific error: 111 | 112 | ```go 113 | // Assume err is a multierror value 114 | err := somefunc() 115 | 116 | // We want to know if "err" has a "RichErrorType" in it and extract it. 117 | var errRich RichErrorType 118 | if errors.As(err, &errRich) { 119 | // It has it, and now errRich is populated. 120 | } 121 | ``` 122 | 123 | **Checking for an exact error value** 124 | 125 | Some errors are returned as exact errors such as the [`ErrNotExist`](https://golang.org/pkg/os/#pkg-variables) 126 | error in the `os` package. You can check if this error is present by using 127 | the standard [`errors.Is`](https://golang.org/pkg/errors/#Is) function. 128 | 129 | ```go 130 | // Assume err is a multierror value 131 | err := somefunc() 132 | if errors.Is(err, os.ErrNotExist) { 133 | // err contains os.ErrNotExist 134 | } 135 | ``` 136 | 137 | **Returning a multierror only if there are errors** 138 | 139 | If you build a `multierror.Error`, you can use the `ErrorOrNil` function 140 | to return an `error` implementation only if there are errors to return: 141 | 142 | ```go 143 | var result *multierror.Error 144 | 145 | // ... accumulate errors here 146 | 147 | // Return the `error` only if errors were added to the multierror, otherwise 148 | // return nil since there are no errors. 149 | return result.ErrorOrNil() 150 | ``` 151 | -------------------------------------------------------------------------------- /append.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package multierror 5 | 6 | // Append is a helper function that will append more errors 7 | // onto an Error in order to create a larger multi-error. 8 | // 9 | // If err is not a multierror.Error, then it will be turned into 10 | // one. If any of the errs are multierr.Error, they will be flattened 11 | // one level into err. 12 | // Any nil errors within errs will be ignored. If err is nil, a new 13 | // *Error will be returned. 14 | func Append(err error, errs ...error) *Error { 15 | switch err := err.(type) { 16 | case *Error: 17 | // Typed nils can reach here, so initialize if we are nil 18 | if err == nil { 19 | err = new(Error) 20 | } 21 | 22 | // Go through each error and flatten 23 | for _, e := range errs { 24 | switch e := e.(type) { 25 | case *Error: 26 | if e != nil { 27 | err.Errors = append(err.Errors, e.Errors...) 28 | } 29 | default: 30 | if e != nil { 31 | err.Errors = append(err.Errors, e) 32 | } 33 | } 34 | } 35 | 36 | return err 37 | default: 38 | newErrs := make([]error, 0, len(errs)+1) 39 | if err != nil { 40 | newErrs = append(newErrs, err) 41 | } 42 | newErrs = append(newErrs, errs...) 43 | 44 | return Append(&Error{}, newErrs...) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /append_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package multierror 5 | 6 | import ( 7 | "errors" 8 | "testing" 9 | ) 10 | 11 | func TestAppend_Error(t *testing.T) { 12 | original := &Error{ 13 | Errors: []error{errors.New("foo")}, 14 | } 15 | 16 | result := Append(original, errors.New("bar")) 17 | if len(result.Errors) != 2 { 18 | t.Fatalf("wrong len: %d", len(result.Errors)) 19 | } 20 | 21 | original = &Error{} 22 | result = Append(original, errors.New("bar")) 23 | if len(result.Errors) != 1 { 24 | t.Fatalf("wrong len: %d", len(result.Errors)) 25 | } 26 | 27 | // Test when a typed nil is passed 28 | var e *Error 29 | result = Append(e, errors.New("baz")) 30 | if len(result.Errors) != 1 { 31 | t.Fatalf("wrong len: %d", len(result.Errors)) 32 | } 33 | 34 | // Test flattening 35 | original = &Error{ 36 | Errors: []error{errors.New("foo")}, 37 | } 38 | 39 | result = Append(original, Append(nil, errors.New("foo"), errors.New("bar"))) 40 | if len(result.Errors) != 3 { 41 | t.Fatalf("wrong len: %d", len(result.Errors)) 42 | } 43 | } 44 | 45 | func TestAppend_NilError(t *testing.T) { 46 | var err error 47 | result := Append(err, errors.New("bar")) 48 | if len(result.Errors) != 1 { 49 | t.Fatalf("wrong len: %d", len(result.Errors)) 50 | } 51 | } 52 | 53 | func TestAppend_NilErrorArg(t *testing.T) { 54 | var err error 55 | var nilErr *Error 56 | result := Append(err, nilErr) 57 | if len(result.Errors) != 0 { 58 | t.Fatalf("wrong len: %d", len(result.Errors)) 59 | } 60 | } 61 | 62 | func TestAppend_NilErrorIfaceArg(t *testing.T) { 63 | var err error 64 | var nilErr error 65 | result := Append(err, nilErr) 66 | if len(result.Errors) != 0 { 67 | t.Fatalf("wrong len: %d", len(result.Errors)) 68 | } 69 | } 70 | 71 | func TestAppend_NonError(t *testing.T) { 72 | original := errors.New("foo") 73 | result := Append(original, errors.New("bar")) 74 | if len(result.Errors) != 2 { 75 | t.Fatalf("wrong len: %d", len(result.Errors)) 76 | } 77 | } 78 | 79 | func TestAppend_NonError_Error(t *testing.T) { 80 | original := errors.New("foo") 81 | result := Append(original, Append(nil, errors.New("bar"))) 82 | if len(result.Errors) != 2 { 83 | t.Fatalf("wrong len: %d", len(result.Errors)) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /flatten.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package multierror 5 | 6 | // Flatten flattens the given error, merging any *Errors together into 7 | // a single *Error. 8 | func Flatten(err error) error { 9 | // If it isn't an *Error, just return the error as-is 10 | if _, ok := err.(*Error); !ok { 11 | return err 12 | } 13 | 14 | // Otherwise, make the result and flatten away! 15 | flatErr := new(Error) 16 | flatten(err, flatErr) 17 | return flatErr 18 | } 19 | 20 | func flatten(err error, flatErr *Error) { 21 | switch err := err.(type) { 22 | case *Error: 23 | for _, e := range err.Errors { 24 | flatten(e, flatErr) 25 | } 26 | default: 27 | flatErr.Errors = append(flatErr.Errors, err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /flatten_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package multierror 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "reflect" 10 | "testing" 11 | ) 12 | 13 | func TestFlatten(t *testing.T) { 14 | original := &Error{ 15 | Errors: []error{ 16 | errors.New("one"), 17 | &Error{ 18 | Errors: []error{ 19 | errors.New("two"), 20 | &Error{ 21 | Errors: []error{ 22 | errors.New("three"), 23 | }, 24 | }, 25 | }, 26 | }, 27 | }, 28 | } 29 | 30 | expected := `3 errors occurred: 31 | * one 32 | * two 33 | * three 34 | 35 | ` 36 | actual := fmt.Sprintf("%s", Flatten(original)) 37 | 38 | if expected != actual { 39 | t.Fatalf("expected: %s, got: %s", expected, actual) 40 | } 41 | } 42 | 43 | func TestFlatten_nonError(t *testing.T) { 44 | err := errors.New("foo") 45 | actual := Flatten(err) 46 | if !reflect.DeepEqual(actual, err) { 47 | t.Fatalf("bad: %#v", actual) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /format.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package multierror 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | // ErrorFormatFunc is a function callback that is called by Error to 12 | // turn the list of errors into a string. 13 | type ErrorFormatFunc func([]error) string 14 | 15 | // ListFormatFunc is a basic formatter that outputs the number of errors 16 | // that occurred along with a bullet point list of the errors. 17 | func ListFormatFunc(es []error) string { 18 | if len(es) == 1 { 19 | return fmt.Sprintf("1 error occurred:\n\t* %s\n\n", es[0]) 20 | } 21 | 22 | points := make([]string, len(es)) 23 | for i, err := range es { 24 | points[i] = fmt.Sprintf("* %s", err) 25 | } 26 | 27 | return fmt.Sprintf( 28 | "%d errors occurred:\n\t%s\n\n", 29 | len(es), strings.Join(points, "\n\t")) 30 | } 31 | -------------------------------------------------------------------------------- /format_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package multierror 5 | 6 | import ( 7 | "errors" 8 | "testing" 9 | ) 10 | 11 | func TestListFormatFuncSingle(t *testing.T) { 12 | expected := `1 error occurred: 13 | * foo 14 | 15 | ` 16 | 17 | errors := []error{ 18 | errors.New("foo"), 19 | } 20 | 21 | actual := ListFormatFunc(errors) 22 | if actual != expected { 23 | t.Fatalf("bad: %#v", actual) 24 | } 25 | } 26 | 27 | func TestListFormatFuncMultiple(t *testing.T) { 28 | expected := `2 errors occurred: 29 | * foo 30 | * bar 31 | 32 | ` 33 | 34 | errors := []error{ 35 | errors.New("foo"), 36 | errors.New("bar"), 37 | } 38 | 39 | actual := ListFormatFunc(errors) 40 | if actual != expected { 41 | t.Fatalf("bad: %#v", actual) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/go-multierror 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp/go-multierror/1ee6e1a1957a8ca61fb9186bab5525fb83763c1b/go.sum -------------------------------------------------------------------------------- /group.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package multierror 5 | 6 | import "sync" 7 | 8 | // Group is a collection of goroutines which return errors that need to be 9 | // coalesced. 10 | type Group struct { 11 | mutex sync.Mutex 12 | err *Error 13 | wg sync.WaitGroup 14 | } 15 | 16 | // Go calls the given function in a new goroutine. 17 | // 18 | // If the function returns an error it is added to the group multierror which 19 | // is returned by Wait. 20 | func (g *Group) Go(f func() error) { 21 | g.wg.Add(1) 22 | 23 | go func() { 24 | defer g.wg.Done() 25 | 26 | if err := f(); err != nil { 27 | g.mutex.Lock() 28 | g.err = Append(g.err, err) 29 | g.mutex.Unlock() 30 | } 31 | }() 32 | } 33 | 34 | // Wait blocks until all function calls from the Go method have returned, then 35 | // returns the multierror. 36 | func (g *Group) Wait() *Error { 37 | g.wg.Wait() 38 | g.mutex.Lock() 39 | defer g.mutex.Unlock() 40 | return g.err 41 | } 42 | -------------------------------------------------------------------------------- /group_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package multierror 5 | 6 | import ( 7 | "errors" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestGroup(t *testing.T) { 13 | err1 := errors.New("group_test: 1") 14 | err2 := errors.New("group_test: 2") 15 | 16 | cases := []struct { 17 | errs []error 18 | nilResult bool 19 | }{ 20 | {errs: []error{}, nilResult: true}, 21 | {errs: []error{nil}, nilResult: true}, 22 | {errs: []error{err1}}, 23 | {errs: []error{err1, nil}}, 24 | {errs: []error{err1, nil, err2}}, 25 | } 26 | 27 | for _, tc := range cases { 28 | var g Group 29 | 30 | for _, err := range tc.errs { 31 | err := err 32 | g.Go(func() error { return err }) 33 | 34 | } 35 | 36 | gErr := g.Wait() 37 | if gErr != nil { 38 | for i := range tc.errs { 39 | if tc.errs[i] != nil && !strings.Contains(gErr.Error(), tc.errs[i].Error()) { 40 | t.Fatalf("expected error to contain %q, actual: %v", tc.errs[i].Error(), gErr) 41 | } 42 | } 43 | } else if !tc.nilResult { 44 | t.Fatalf("Group.Wait() should not have returned nil for errs: %v", tc.errs) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /multierror.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package multierror 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | // Error is an error type to track multiple errors. This is used to 12 | // accumulate errors in cases and return them as a single "error". 13 | type Error struct { 14 | Errors []error 15 | ErrorFormat ErrorFormatFunc 16 | } 17 | 18 | func (e *Error) Error() string { 19 | fn := e.ErrorFormat 20 | if fn == nil { 21 | fn = ListFormatFunc 22 | } 23 | 24 | return fn(e.Errors) 25 | } 26 | 27 | // ErrorOrNil returns an error interface if this Error represents 28 | // a list of errors, or returns nil if the list of errors is empty. This 29 | // function is useful at the end of accumulation to make sure that the value 30 | // returned represents the existence of errors. 31 | func (e *Error) ErrorOrNil() error { 32 | if e == nil { 33 | return nil 34 | } 35 | if len(e.Errors) == 0 { 36 | return nil 37 | } 38 | 39 | return e 40 | } 41 | 42 | func (e *Error) GoString() string { 43 | return fmt.Sprintf("*%#v", *e) 44 | } 45 | 46 | // WrappedErrors returns the list of errors that this Error is wrapping. It is 47 | // an implementation of the errwrap.Wrapper interface so that multierror.Error 48 | // can be used with that library. 49 | // 50 | // This method is not safe to be called concurrently. Unlike accessing the 51 | // Errors field directly, this function also checks if the multierror is nil to 52 | // prevent a null-pointer panic. It satisfies the errwrap.Wrapper interface. 53 | func (e *Error) WrappedErrors() []error { 54 | if e == nil { 55 | return nil 56 | } 57 | return e.Errors 58 | } 59 | 60 | // Unwrap returns an error from Error (or nil if there are no errors). 61 | // This error returned will further support Unwrap to get the next error, 62 | // etc. The order will match the order of Errors in the multierror.Error 63 | // at the time of calling. 64 | // 65 | // The resulting error supports errors.As/Is/Unwrap so you can continue 66 | // to use the stdlib errors package to introspect further. 67 | // 68 | // This will perform a shallow copy of the errors slice. Any errors appended 69 | // to this error after calling Unwrap will not be available until a new 70 | // Unwrap is called on the multierror.Error. 71 | func (e *Error) Unwrap() error { 72 | // If we have no errors then we do nothing 73 | if e == nil || len(e.Errors) == 0 { 74 | return nil 75 | } 76 | 77 | // If we have exactly one error, we can just return that directly. 78 | if len(e.Errors) == 1 { 79 | return e.Errors[0] 80 | } 81 | 82 | // Shallow copy the slice 83 | errs := make([]error, len(e.Errors)) 84 | copy(errs, e.Errors) 85 | return chain(errs) 86 | } 87 | 88 | // chain implements the interfaces necessary for errors.Is/As/Unwrap to 89 | // work in a deterministic way with multierror. A chain tracks a list of 90 | // errors while accounting for the current represented error. This lets 91 | // Is/As be meaningful. 92 | // 93 | // Unwrap returns the next error. In the cleanest form, Unwrap would return 94 | // the wrapped error here but we can't do that if we want to properly 95 | // get access to all the errors. Instead, users are recommended to use 96 | // Is/As to get the correct error type out. 97 | // 98 | // Precondition: []error is non-empty (len > 0) 99 | type chain []error 100 | 101 | // Error implements the error interface 102 | func (e chain) Error() string { 103 | return e[0].Error() 104 | } 105 | 106 | // Unwrap implements errors.Unwrap by returning the next error in the 107 | // chain or nil if there are no more errors. 108 | func (e chain) Unwrap() error { 109 | if len(e) == 1 { 110 | return nil 111 | } 112 | 113 | return e[1:] 114 | } 115 | 116 | // As implements errors.As by attempting to map to the current value. 117 | func (e chain) As(target interface{}) bool { 118 | return errors.As(e[0], target) 119 | } 120 | 121 | // Is implements errors.Is by comparing the current value directly. 122 | func (e chain) Is(target error) bool { 123 | return errors.Is(e[0], target) 124 | } 125 | -------------------------------------------------------------------------------- /multierror_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package multierror 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "reflect" 10 | "testing" 11 | ) 12 | 13 | func TestError_Impl(t *testing.T) { 14 | var _ error = new(Error) 15 | } 16 | 17 | func TestErrorError_custom(t *testing.T) { 18 | errors := []error{ 19 | errors.New("foo"), 20 | errors.New("bar"), 21 | } 22 | 23 | fn := func(es []error) string { 24 | return "foo" 25 | } 26 | 27 | multi := &Error{Errors: errors, ErrorFormat: fn} 28 | if multi.Error() != "foo" { 29 | t.Fatalf("bad: %s", multi.Error()) 30 | } 31 | } 32 | 33 | func TestErrorError_default(t *testing.T) { 34 | expected := `2 errors occurred: 35 | * foo 36 | * bar 37 | 38 | ` 39 | 40 | errors := []error{ 41 | errors.New("foo"), 42 | errors.New("bar"), 43 | } 44 | 45 | multi := &Error{Errors: errors} 46 | if multi.Error() != expected { 47 | t.Fatalf("bad: %s", multi.Error()) 48 | } 49 | } 50 | 51 | func TestErrorErrorOrNil(t *testing.T) { 52 | err := new(Error) 53 | if err.ErrorOrNil() != nil { 54 | t.Fatalf("bad: %#v", err.ErrorOrNil()) 55 | } 56 | 57 | err.Errors = []error{errors.New("foo")} 58 | if v := err.ErrorOrNil(); v == nil { 59 | t.Fatal("should not be nil") 60 | } else if !reflect.DeepEqual(v, err) { 61 | t.Fatalf("bad: %#v", v) 62 | } 63 | } 64 | 65 | func TestErrorWrappedErrors(t *testing.T) { 66 | errors := []error{ 67 | errors.New("foo"), 68 | errors.New("bar"), 69 | } 70 | 71 | multi := &Error{Errors: errors} 72 | if !reflect.DeepEqual(multi.Errors, multi.WrappedErrors()) { 73 | t.Fatalf("bad: %s", multi.WrappedErrors()) 74 | } 75 | 76 | multi = nil 77 | if err := multi.WrappedErrors(); err != nil { 78 | t.Fatalf("bad: %#v", multi) 79 | } 80 | } 81 | 82 | func TestErrorUnwrap(t *testing.T) { 83 | t.Run("with errors", func(t *testing.T) { 84 | err := &Error{Errors: []error{ 85 | errors.New("foo"), 86 | errors.New("bar"), 87 | errors.New("baz"), 88 | }} 89 | 90 | var current error = err 91 | for i := 0; i < len(err.Errors); i++ { 92 | current = errors.Unwrap(current) 93 | if !errors.Is(current, err.Errors[i]) { 94 | t.Fatal("should be next value") 95 | } 96 | } 97 | 98 | if errors.Unwrap(current) != nil { 99 | t.Fatal("should be nil at the end") 100 | } 101 | }) 102 | 103 | t.Run("with no errors", func(t *testing.T) { 104 | err := &Error{Errors: nil} 105 | if errors.Unwrap(err) != nil { 106 | t.Fatal("should be nil") 107 | } 108 | }) 109 | 110 | t.Run("with nil multierror", func(t *testing.T) { 111 | var err *Error 112 | if errors.Unwrap(err) != nil { 113 | t.Fatal("should be nil") 114 | } 115 | }) 116 | } 117 | 118 | func TestErrorIs(t *testing.T) { 119 | errBar := errors.New("bar") 120 | 121 | t.Run("with errBar", func(t *testing.T) { 122 | err := &Error{Errors: []error{ 123 | errors.New("foo"), 124 | errBar, 125 | errors.New("baz"), 126 | }} 127 | 128 | if !errors.Is(err, errBar) { 129 | t.Fatal("should be true") 130 | } 131 | }) 132 | 133 | t.Run("with errBar wrapped by fmt.Errorf", func(t *testing.T) { 134 | err := &Error{Errors: []error{ 135 | errors.New("foo"), 136 | fmt.Errorf("errorf: %w", errBar), 137 | errors.New("baz"), 138 | }} 139 | 140 | if !errors.Is(err, errBar) { 141 | t.Fatal("should be true") 142 | } 143 | }) 144 | 145 | t.Run("without errBar", func(t *testing.T) { 146 | err := &Error{Errors: []error{ 147 | errors.New("foo"), 148 | errors.New("baz"), 149 | }} 150 | 151 | if errors.Is(err, errBar) { 152 | t.Fatal("should be false") 153 | } 154 | }) 155 | } 156 | 157 | func TestErrorAs(t *testing.T) { 158 | match := &nestedError{} 159 | 160 | t.Run("with the value", func(t *testing.T) { 161 | err := &Error{Errors: []error{ 162 | errors.New("foo"), 163 | match, 164 | errors.New("baz"), 165 | }} 166 | 167 | var target *nestedError 168 | if !errors.As(err, &target) { 169 | t.Fatal("should be true") 170 | } 171 | if target == nil { 172 | t.Fatal("target should not be nil") 173 | } 174 | }) 175 | 176 | t.Run("with the value wrapped by fmt.Errorf", func(t *testing.T) { 177 | err := &Error{Errors: []error{ 178 | errors.New("foo"), 179 | fmt.Errorf("errorf: %w", match), 180 | errors.New("baz"), 181 | }} 182 | 183 | var target *nestedError 184 | if !errors.As(err, &target) { 185 | t.Fatal("should be true") 186 | } 187 | if target == nil { 188 | t.Fatal("target should not be nil") 189 | } 190 | }) 191 | 192 | t.Run("without the value", func(t *testing.T) { 193 | err := &Error{Errors: []error{ 194 | errors.New("foo"), 195 | errors.New("baz"), 196 | }} 197 | 198 | var target *nestedError 199 | if errors.As(err, &target) { 200 | t.Fatal("should be false") 201 | } 202 | if target != nil { 203 | t.Fatal("target should be nil") 204 | } 205 | }) 206 | } 207 | 208 | // nestedError implements error and is used for tests. 209 | type nestedError struct{} 210 | 211 | func (*nestedError) Error() string { return "" } 212 | -------------------------------------------------------------------------------- /prefix.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package multierror 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | // Prefix is a helper function that will prefix some text 11 | // to the given error. If the error is a multierror.Error, then 12 | // it will be prefixed to each wrapped error. 13 | // 14 | // This is useful to use when appending multiple multierrors 15 | // together in order to give better scoping. 16 | func Prefix(err error, prefix string) error { 17 | if err == nil { 18 | return nil 19 | } 20 | 21 | switch err := err.(type) { 22 | case *Error: 23 | // Typed nils can reach here, so initialize if we are nil 24 | if err == nil { 25 | err = new(Error) 26 | } 27 | 28 | // Wrap each of the errors 29 | for i, e := range err.Errors { 30 | err.Errors[i] = fmt.Errorf("%s %s", prefix, e) 31 | } 32 | 33 | return err 34 | default: 35 | return fmt.Errorf("%s %s", prefix, err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /prefix_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package multierror 5 | 6 | import ( 7 | "errors" 8 | "testing" 9 | ) 10 | 11 | func TestPrefix_Error(t *testing.T) { 12 | original := &Error{ 13 | Errors: []error{errors.New("foo")}, 14 | } 15 | 16 | result := Prefix(original, "bar") 17 | if result.(*Error).Errors[0].Error() != "bar foo" { 18 | t.Fatalf("bad: %s", result) 19 | } 20 | } 21 | 22 | func TestPrefix_NilError(t *testing.T) { 23 | var err error 24 | result := Prefix(err, "bar") 25 | if result != nil { 26 | t.Fatalf("bad: %#v", result) 27 | } 28 | } 29 | 30 | func TestPrefix_NonError(t *testing.T) { 31 | original := errors.New("foo") 32 | result := Prefix(original, "bar") 33 | if result == nil { 34 | t.Fatal("error result was nil") 35 | } 36 | if result.Error() != "bar foo" { 37 | t.Fatalf("bad: %s", result) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sort.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package multierror 5 | 6 | // Len implements sort.Interface function for length 7 | func (err *Error) Len() int { 8 | if err == nil { 9 | return 0 10 | } 11 | 12 | return len(err.Errors) 13 | } 14 | 15 | // Swap implements sort.Interface function for swapping elements 16 | func (err Error) Swap(i, j int) { 17 | err.Errors[i], err.Errors[j] = err.Errors[j], err.Errors[i] 18 | } 19 | 20 | // Less implements sort.Interface function for determining order 21 | func (err Error) Less(i, j int) bool { 22 | return err.Errors[i].Error() < err.Errors[j].Error() 23 | } 24 | -------------------------------------------------------------------------------- /sort_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package multierror 5 | 6 | import ( 7 | "errors" 8 | "reflect" 9 | "sort" 10 | "testing" 11 | ) 12 | 13 | func TestSortSingle(t *testing.T) { 14 | errFoo := errors.New("foo") 15 | 16 | expected := []error{ 17 | errFoo, 18 | } 19 | 20 | err := &Error{ 21 | Errors: []error{ 22 | errFoo, 23 | }, 24 | } 25 | 26 | sort.Sort(err) 27 | if !reflect.DeepEqual(err.Errors, expected) { 28 | t.Fatalf("bad: %#v", err) 29 | } 30 | } 31 | 32 | func TestSortMultiple(t *testing.T) { 33 | errBar := errors.New("bar") 34 | errBaz := errors.New("baz") 35 | errFoo := errors.New("foo") 36 | 37 | expected := []error{ 38 | errBar, 39 | errBaz, 40 | errFoo, 41 | } 42 | 43 | err := &Error{ 44 | Errors: []error{ 45 | errFoo, 46 | errBar, 47 | errBaz, 48 | }, 49 | } 50 | 51 | sort.Sort(err) 52 | if !reflect.DeepEqual(err.Errors, expected) { 53 | t.Fatalf("bad: %#v", err) 54 | } 55 | } 56 | --------------------------------------------------------------------------------