-
Notifications
You must be signed in to change notification settings - Fork 18.4k
Description
I propose to add errors.Errors
(note: I don't like the name either, please think of it as a placeholder) function to handle errors that contain multiple errors.
Problem
Upon trying out go1.20rc1's new features around errors.Join
, I could not immediately find out how one should access individual errors in sequence:
err := interestingFunc()
for err := range err.Errors() { // obviously does not compile: Errors does not exist
// do something with err
}
This sort of sequential scan is necessary if the contained error does not implement a distinct type or its own Is(error) bool
function, as you cannot extract out plain errors created by errors.New
/fmt.Errorf
using As()
. You also would need to know what kind of errors exists before hand in order to use As
, which is not always the case.
It can be argued that users are free to use something like interface{ Unwrap() []error }
to convert the type of the error first, and then proceed to extract the container errors:
if me, ok := err.(interface{ Unwrap() []error }); ok {
for _, err := range me.Unwrap() {
...
}
}
This works, but if the error returned could be either a plain error or an error containing other errors, you would have to write two code paths to handle the error:
switch err := err.(type) {
case interface{ Unwrap() []error }:
... // act upon each errors that are contained
default:
... // act upon the error itself
}
This forces the user to write much boilerplate code. But if we add a simple function to abstract out the above type conversion, the user experience can be greatly enhanced.
Proposed solution
Implement a thin wrapper function around error
types:
package errors
// Errors can be used to against an `error` that contains one or more errors in it (created
// using either `errors.Join` or `fmt.Errorf` with multiple uses of "%w").
//
// If the error implement `Unwrap() []error`, the result of calling `Unwrap` on `err` is returned.
// Only the immediate child errors are returned: this function will not traverse into the child
// errors to find out all of the errors contained in the error.
//
// If the error specified as the argument is a plain error that does not contain other errors,
// the error itself is used as the sole element within the return slice.
func Errors(err error) []error {
switch err := err.(type) {
case interface { Unwrap() []error }:
return err.Unwrap()
default:
return []error{err}
}
}
Pros/Cons
Pros:
- Cleaner way to access joined errors: users will no longer need to be aware of the difference between a plain error and errors containing multiple child errors
- Less boilerplate code for something that every developer will need to write when dealing with errors created by
errors.Join
- An explicit function is more discoverable
Cons:
- There's a slight asymmetry in the behavior: whereas the
error
value itself is returned for plain errors, the result for errors containing other errors does not include the original error itself.