-
Notifications
You must be signed in to change notification settings - Fork 17.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: errors: add Errors as a standard way to represent multiple errors as a single error #47811
Comments
If
If we use
|
Thanks for the feedback @neild! Handling wrapped errors certainly complicates things. Full disclosure: I don't generally use wrapped errors in production, so I would defer to the wisdom of those who do. That said, here are my thoughts: I think The If all of the |
Thinking some more about your questions, maybe there's a simpler solution that wouldn't require modifying // 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) Errors {
if len(errs) == 0 {
return nil
}
var es errors
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
}
// errors combines many errors into a single error
type errors []error
// Unwrap works with errors.Unwrap
func (es errors) Unwrap() error {
if len(es) > 0 {
return es[1:]
}
return nil
}
// Is works with errors.Is
func (es errors) Is(err error) bool {
if len(es) == 0 {
return err != nil
}
return err.Error() == es[0].Error()
}
// Errors implements the Errors interface
func (es errors) Errors() []error {
return []error(es)
}
// Error implements the error interface
func (es errors) Error() string {
return fmt.Sprintf("%v", []error(es))
} |
First let me start out by saying that there seems to be a clear need for this feature as seen by looking for 'multi error' on pkg.go.dev. |
@marksalpeter Your implementation of Is and Unwrap is incorrect here. For example if Errors only contains a single error Unwrap discards it instead of returning it unwrapped and Is does a string comparison on just the first error?! |
@D1CED My apologies. As I said before, I'm unfamiliar with the use case. My understanding of |
Uh, I think my statement above was partially incorrect. Your implementation kind of does work when changing the comparison. But it seems like we have a very different idea here. My suggested implementation would be
and I'd not implement an Unwrap method at all. Edit: Got a little confused with the names here. The package is called errors and type is called errors. Maybe we should call it multiError as this seems to be a common name. |
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. |
@D1CED I'm not sure that recursively calling |
@D1CED You can see on line 90 of // As works with errors.As
func (es errors) As(target interface{}) bool {
if len(es) == 0 {
return false
}
err := es[0]
val := reflectlite.ValueOf(target)
targetType := val.Type()
if reflectlite.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
return false
} I also think the version of |
I've added the example implementations of As and Is to the original example in the proposal with a note at the top. Thanks again for pointing out this use case @neild 🙏 |
Ok, so I traced this and the recursive call is definitely more correct than either of my previous attempts. Sorry about that 😅. I still think we can use |
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. |
Thanks for bringing other package implementations to my attention. It looks like hashicorp has a pretty good one that handles the Do you have a reference to the go 1.13 discussion around multierrors @neild? Maybe this will help me get up to speed. As for formatting the errors, I’m inclined towards an approach with a global |
Not any more, sorry. The key points as I recall them were:
This all argued that we should avoid standardizing a multierror type at the time. My personal experience has been that general-purpose error aggregating types push complexity to the users of an API that is better handled internally, and that it is usually best to either explicitly return an |
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. |
@neild, I think the use of an interface can sidestep the issues of specific formatting requirements and even incompatible unwrapping requirements, much like the As for there being "no obstacle to implementing multierror outside of the standard library", this is also true of many standard library packages. I think the real question is whether or not this is a common enough use case to warrant a standard solution. I agree with @D1CED that the answer is a firm "yes". While its possible to pass a @neild @carlmjohnson Just so I understand the problem set better, do either of you have an example of incompatible unwrapping requirements? |
Suppose you have error 1 which is a normal error and error 2 which |
Thanks for clarifying @carlmjohnson
This is a good point and it kind of unpacks what @neild said in the beginning. Imagine traversing an error stack like this: err3 := fmt.Errorf("d %w", errors.New("e"))
err2 := fmt.Errorf("b %w", &customError{ "c" })
err1 := fmt.Errorf("a %w", errors.NewErrors(err2, err3))
// Traverse the error stack
for err := err1; err != nil; err = errors.Unwrap(err1) {
fmt.Println(err1)
} Ether the output looks like this: a [b c d e]
[b c d e]
[c d e]
[d e]
[e] Or it looks like this: a [b c d e]
[b c d e] Maybe the second way is more clear. Multierrors are wrapped together, so they should be unwrapped together. Therefore, its better traverse them differently, using the The question then becomes, "Does
|
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 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. |
This proposal has been added to the active column of the proposals project |
Based on the discussion above, especially #47811 (comment), it sounds like there's no good single answer for this, so we should leave it to continue to be provided by packages outside the standard library. |
No change in consensus, so declined. |
For people interested: there is currently a proposal to add a similar concept to python PEP-654. |
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 Again, a big thank you to everyone involved. This was, if nothing else, enlightening 🙏 |
I'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. |
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 don't like multierrors where flattening is done automatically/implicitly. I need to be in control of whether something gets flattened or nested, and also how such an error is printed. But this is mainly an API question. Since I don't use unwrap, I'm fine if it's missing. Though it feels a bit odd that As() iterates the error left-to-right, but unwrap doesn't. https://github.com/maja42/multierr solves this and makes sure that As() and Unwrap() are consistent. |
I don't have the problem with flattening but it seems like it would be easier for that to be done it in the multierror factory What is the meaning of an error list of length 0? Is there a way to define this with an interface and helpers so there can be multiple multierrors that interoperate but format differently? Maybe something like: type Multi interface {
error
Is(error) bool
As(any) bool
// bad name but disallows both Unwraps
// being implemented on the same type
Unwrap() []error
}
// return nil if not a Multi error?
func Errors(error) []error Defining just that and punting on an implementation would give time for others to experiment and settle on best formatting practices. However, I suppose fmt.Errorf with multiple %w patterns could be defined naturally as it already returns a different error type depending on the number of %w included. |
Early versions of my multierror package defined a multierror as |
Wouldn't it suffice to specify in the interface that you must return a nonzero length and have the helper panic if len == 0? |
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 My own modest proposal: An error wraps multiple errors if its type has the method
Reusing the name The
The
The This proposal:
|
|
The discrepancy between the
To be precise, the traversal of @maja42, I think you could use this to simplify your implementation. By removing |
It's also not possible to construct an empty multierr, or a multierr containing nil-errors. (It will always turn out to be nil itself). This is a nice feature that prevents questions like "what if the error-list is empty". My goal was that a multierr behaves exactly like a normal error, and the library itself also doesn't care what kind of error you feed it in it's various methods. In most cases (especially for validation purposes) a caller of a function doesn't care if the returned error is a single error or a multi error. So existing code can start using multi-errors instead (and producing more complete error messages for the user) without the fear of breaking anything, be it any method signatures/APIs, or code that unwraps/inspects the underlying causes. In fact, my Inspect() method is not a member-method but a global function, also taking a single error argument and not caring if it's actually a multierr or a conventional error (which will result in a slice of length 1). A lot of thoughts went into it (this is actually the 4th iteration of the library), and after years of using it in production I'm pretty happy with it - at least for the projects I'm working on. (There's a minor inconvenience that i might fix in the future though). So if go started with it's own multi-error standard/package, there is a high chance that instead of replacing my library, I instead add compatibility with the go standard - completely defeating its purpose. (Under the assumption that the standard library will have less features/functionality than mine). And if other library authors think the same ... this proposal should indeed be kept close. Go could define an interface/minimum set of functions a multierr must implement to get some common ground - but considering that methods should continue to return |
@maja42, thanks for your detailed comparison. Say we changed the name of the new Unwrap function to something else ( Those seem to be the two properties of this proposal that don't match your package. I still think you'd want your own package, because it lets you control formatting. But what if you just deleted the Is, As and Unwrap methods, and added an Unwraps instead? |
@neild, one problem with |
Ah, I understand. How would the error-string look like, after constructing a multierr with Join? |
After 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 |
Filed #52936 as a different way to think about printing multierrs. |
Another new proposal at #53435. |
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
Example Usage
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 🙏
The text was updated successfully, but these errors were encountered: