multierr
is a package that allows combining multiple errors into a single error
type.
This allows functions to return multiple errors at once.
Callers can either use the returned multi-error as a conventional error (which is printed as a nice human-readable string), or continue working with it by appending or unwrapping individual errors.
When validating configurations or user-input, it's always a great user-experience to see all problems at once. Just append all individual errors to a multi-error and return it to the user.
When implementing APIs (be it a WebServer's REST-API, a protobuf RPC API or any other interface), validating incoming data and returning all problems at once greatly improves a developer's quality of life.
No longer do API-users need to call an endpoint just to receive the next error they need to fix.
Sometimes, multiple concurrently-running go-routines can each return an error. Which error should you report? The first one? What about the others, log them or ignore them? A multi-error can simply collect all those errors and return them at once.
type Input struct {
Name string
Age int
}
func (i *Input) Validate() error {
var valErr error
if i.Name == "" {
valErr = multierr.Append(valErr, errors.New("missing name"))
}
if i.Age < 18 {
valErr = multierr.Append(valErr, errors.New("too young"))
}
return valErr
}
This prints the following output:
2 errors occurred:
- missing name
- too young
If you instead return multierr.Titled(valErr, "Invalid input:")
, you can get the following output:
Invalid input:
- missing name
- too young
Alternatively, you can prefix each error via multierr.Prefixed(valErr, "Invalid input: ")
to get the following output:
Invalid input: missing name
Invalid input: too young
When validating nested structures, you often receive errors from sub-validators. The same can happen when calling functions.
These cases can be handled in 4 different ways, all of them producing great error messages:
type Input struct {
Name string
Age int
Address Address
}
type Address struct {
City string
Street string
}
func (i *Input) Validate() error {
var valErr error
if i.Name == "" {
valErr = multierr.Append(valErr, errors.New("missing name"))
}
if i.Age < 18 {
valErr = multierr.Append(valErr, errors.New("too young"))
}
valErr = multierr.Append(valErr, i.Address.Validate())
return multierr.Titled(valErr, "invalid input:")
}
func (a *Address) Validate() error {
var valErr error
if a.City == "" {
valErr = multierr.Append(valErr, errors.New("missing city"))
}
if a.Street == "" {
valErr = multierr.Append(valErr, errors.New("missing street"))
}
return valErr
}
This is the simplest version.
And you just got rid of those nasty if-error-checks.
You don't need to check for nil-errors when validating the address.
If there is no error, Append()
will simply do nothing.
You get the following error message:
invalid input:
- missing name
- too young
- 2 errors occurred:
- missing city
- missing street
You can get a slightly better error message by choosing your own title.
Replace the address validation with this piece of code:
err := i.Address.Validate()
valErr = multierr.Append(valErr, multierr.Titled(err, "invalid address:"))
Again - you don't need to check for errors. The Titled
-function simply returns nil
if there was no error.
You get the following output:
invalid input:
- missing name
- too young
- invalid address:
- missing city
- missing street
Now, what if you don't want nested error messages? Just merge them!
Replace the address validation with this:
valErr = multierr.Merge(valErr, i.Address.Validate())
You will get the following:
invalid input:
- missing name
- too young
- missing city
- missing street
In the above example, you do not see that city and street are subfields of the address. You can keep that information by using prefixes. Replace the address validation with this:
valErr = multierr.MergePrefixed(valErr, "invalid adress: ", i.Address.Validate())
You will get the following:
invalid input:
- missing name
- too young
- invalid adress: missing city
- invalid adress: missing street
And, of course, calling fmt.Errorf()
instead of multierr.Append()
also yields great results.
Perform the address validation as follows:
if err := i.Address.Validate(); err != nil {
valErr = multierr.Append(valErr, fmt.Errorf("invalid address: %s", err))
}
You will get:
invalid input:
- missing name
- too young
- invalid address: 2 errors occurred:
- missing city
- missing street
Sometimes, you just want to format errors differently. And that's entirely possible:
err := multierr.Append(
errors.New("error 1"),
errors.New("error 2"),
)
err.Formatter = func(errs []error) string {
return fmt.Sprintf("there are %d errors", len(errs))
}
This is not feasible if you want to have a different error format globally though.
In that case, you can overwrite the default formatter:
multierr.DefaultFormatter = func(errs []error) string {
return fmt.Sprintf("there are %d errors", len(errs))
}
You can access a list with all sub-errors by simply calling
errList := multierr.Inspect(multiErr)
This also works if the provided argument is not actually a multi-error.
If it's a normal error
, the returned list will have the error as a single element.
Multi-errors support the standard library's errors.Unwrap()
, errors.As()
and errors.Is()
methods.
It's therefore possible to inspect certain root-causes of an error.