go: proposal: errors: add Errors as a standard way to represent multiple errors as a single error
Description
It is common practice in golang to return an error from a func in the event of a problem during its execution (e.g. func myFunc() error { ... }).
However, there are a few cases, such as field validation on a struct, where it is useful to return multiple errors from a function (e.g. func validate(m *Model) []error). Sometimes these errors need to be ‘bubbled up’ through a call stack that only returns one error. So, I find myself having to re-implement custom errors that represent many errors in each of my projects.
I think a generic solution could be added to the standard libraries’ errors package that serves this purpose.
Example Implementation
I’ve updated the example implementation to incorporate the feedback from @neild and @D1CED
package util
import (
"fmt"
)
// Errors are many errors that can be represented as a single error
type Errors interface {
Errors() []error
error
}
// NewErrors combine many errors into a single error
func NewErrors(errs ...error) error {
if lenErrs := len(errs); lenErrs == 0 {
return nil
} else if lenErrs == 1 {
return errs[0]
}
var es manyErrors
for _, err := range errs {
// Merge many slices of errors into a single slice
if errs, ok := err.(Errors); ok {
es = append(es, errs.Errors()...)
continue
}
es = append(es, err)
}
return es
}
// manyErrors combines many errors into a single error
type manyErrors []error
// Is works with errors.Is
func (es errors) Is(find error) bool {
for _, e := range es {
if Is(e, find) {
return true
}
}
return false
}
// As works with errors.As
func (es errors) As(find interface{}) bool {
for _, e := range es {
if As(e, find) {
return true
}
}
return false
}
// Errors implements the Errors interface
func (es manyErrors) Errors() []error {
return []error(es)
}
// Error implements the error interface
func (es manyErrors) Error() string {
return fmt.Sprintf("[%v", []error(es))
}
Example Usage
import "errors"
type Model struct {
FieldOne string `json:"field_one"`
FieldTwo string `json:"field_two"`
FieldThree string `json:"field_three"`
}
func (m *Model) Validate() error {
var errs []error
if len(m.FieldOne) < 1 {
errs = append(errs, errors.New("'field_one' is required"))
}
if len(m.FieldTwo) < 1 {
errs = append(errs, errors.New("'field_two' is required"))
}
if len(m.FieldThree) < 1 {
errs = append(errs, errors.New("'field_three' is required"))
}
return errors.NewErrors(errs...)
}
Conclusion
Whether or not this proposal is accepted, I’m curious how others have approached this problem in the past and if there’s already a commonly referenced solution that I’m unfamiliar with. Thanks 🙏
About this issue
- Original URL
- State: closed
- Created 3 years ago
- Reactions: 5
- Comments: 45 (24 by maintainers)
If
errcontains multiple errors, what doeserrors.Is(err, fs.ErrExist)return?trueif all errors inerrarefs.ErrExist?trueif at least one error inerrisfs.ErrExist?false?If we use
errors.Asto convert a multierror value to a*os.PathError, does it:trueif at least one error inerris an*os.PathError, and extract one of the*os.PathErrors?trueif the only error inerris an*os.PathError?false?After
errors.Join(";", err1, err2), the result would print like this: fmt.Sprintf(“%s; %s”, err1, err2).But the nice thing about the proposal is that you don’t have to use errors.Join if you don’t want to. The important part is the
Unwrapsmethod and function and the changes toIsandAs, allowing multierrors of all kinds to behave the same.I’m still of the opinion that just about every place I’ve seen a multierror used would have been better served by an explicit
[]error. However, this is a common request and there are a number of competing packages providing multierror implementations. If there’s enough commonality between those implementations, that may argue for standardizing on a common interface.My own modest proposal:
An error wraps multiple errors if its type has the method
Reusing the name
Unwrapis proposed by @jimmyfrasche above, and has the nice characteristic of avoiding ambiguity when an error has singular and multiple unwraps. It also avoids changing behavior, since I don’t think any existing multierror implementations implement this method signature. Returning a 0-length list fromUnwrapmeans the error doesn’t wrap anything.The
errors.Splitfunction provides a mechanism for retrieving the original errors:The
errors.Joinfunction provides a simple implementation of a multierr. It does not flatten errors.The
errors.Isanderrors.Asfunctions are updated to unwrap multiple errors. We replace the term “error chain” with “error tree”.Isreports a match if any error in the tree matches.Asfinds the first error in a preorder traversal of the tree that matches.This proposal:
errors.Is/errors.Asrather than leaving it up to multierror implementations to implement their ownIs/Asmethods. This standardizes the tree traversal, and follows the model used for wrapping singular errors.fmt.Errorfshould be updated to take multiple%wand when there’s more than one return a type withUnwrap() []errorI’m sorry I missed this discussion, although at the time I would have probably agreed with the decision. Now I think there are clear answers to the questions posed here. Namely:
This matches Tailscale’s design.
Are any of these controversial? It seems to me that once you get rid of Unwrap, these are all more or less obvious. I was always hung up on figuring out what Unwrap would mean, on the assumption that it would be used a lot. (People used to talk about errors “causes” all the time, meaning the last error in the chain.) My understanding now is that we were successful in promoting Is and As, so that it’s rare to call Unwrap. If that’s correct, then dropping it here will cause little pain.
Formatting is still an open question (here is one cute idea).
To be clear, I’m not asking to reopen this proposal yet, nor yet asking for formatting ideas. I’m conducting a poll: are the semantics I presented wrong for anyone? Because if not, then the main reason for rejection–there are too many ways to do this—is in question.
Filed #52936 as a different way to think about printing multierrs.
@neild, one problem with
Unwrap() []erroris that the implementor must either copy, or risk exposing their internal state to mischief. The latter is probably the right choice but I think we should add “don’t modify the slice” to the method doc.The discrepancy between the
Unwrapmethod and theSplitfunction is unfortunate. We could call them bothUnwraps.Joinshould elide nil errors, returning nil if they’re all nil.To be precise, the traversal of
Asshould be specified as preorder left-to-right.@maja42, I think you could use this to simplify your implementation. By removing
Unwrap, you also can get rid of the allocation and copying you need in order to make achain.Interesting. I think one of the keys has to be that multierror(multierror) gets flattened but multierror(wrapper(multierror)) does not, so that a wrapper is never lost in the process of creating the multierror. Once you do that, one of the main objections I had to this proposal goes away.
I would just like to say to everyone involved that I really appreciate the lively discussion and consideration of this proposal. It uncovered a number of issues, use cases and concerns regarding the unwrapping and equality of errors that I was previously unaware of. And now I have a much deeper understanding of how error wrapping is being used in practice and all of the ways gophers are interested in potentially using a multierror.
I still think there’s an opportunity to bring a more integrated, stripped back, proposal of an
Errorsinterface to the standard library - one that side steps many of these concerns, but is perhaps less useful on its own. I’ll be sure to link that proposal to this one once it’s ready.Again, a big thank you to everyone involved. This was, if nothing else, enlightening 🙏
I used hashicorp for a while, but ran into the same problems as you already discovered:
What happens when appending/merging/flattening errors that are multi-errors? How to format errors? What about As/Is?
A lot of those questions need to be decided on a case-by-case basis (even within the same project), so in the end I wrote my own multierr implementation which I now use for all of my private- and work projects. It covers all those situations gracefully. Only the As/Is/Unwrap behavior can be controversial, which is why I implement
Inspect().Usually I prefer the Append() function, which does not flatten the tree. I only use Merge() (=flatten errors) when writing validation functions on smaller, nested structs.
So yes, this is a pretty common issue. But there’s no universal solution, and already a few libraries. So I don’t think this should be added to the standard library.
Suppose you have error 1 which is a normal error and error 2 which
fmt.Errorf("my prefix: %w", Multierror{err3, err4}). What happens if you combine error 1 and 2? Does it flatten the tree? If it doesn’t flatten the list, you get unexpected results when you try to iterate through all suberrors. But if it does flatten the list, you lose “my prefix” which might be important.I have a multierror package: https://github.com/carlmjohnson/errutil. Mine is better than other people’s because it is compatible with both Hashicorp and Uber multierrors. 😃 More seriously, I think the design space here is a little too ambiguous to fit into the standard library. There isn’t one right way to handle unwrapping.
I don’t know. Any multiple-error type needs to come up with good answers for these questions, however.
Another question without an obviously correct universal answer is what the error string for a collection of errors should look like: Concatenate all the error strings with some separator? One line or multiple lines? Summarize in some fashion, such as deduplicating multiple instances of the same error text?
The fact that there didn’t seem to be an obviously correct universal answer to these questions was a primary reason we didn’t introduce a standard multierror type in Go 1.13. There are now a number of multiple-error types implemented outside the standard library; perhaps considering the experience of these implementations would point at some answers.
One small feature I found when looking at other implementations is that if there is exactly one error it is simply returned unwrapped. This should be considered here too.