Golang 1.20 has been released 🎉
One of the key changes that I noticed reading the release notes, is that it adds support for handling multiple errors natively. This is awesome, and I wanted to dig deeper into how to leverage this.
This is what release notes say:
Wrapping multiple errors
Go 1.20 expands support for error wrapping to permit an error to wrap multiple other errors.
An error e can wrap more than one error by providing anUnwrap
method that returns a[]error
.
Theerrors.Is
anderrors.As
functions have been updated to inspect multiply wrapped errors.
Thefmt.Errorf
function now supports multiple occurrences of the%w
format verb, which will cause it to return an error that wraps all of those error operands.
The new functionerrors.Join
returns an error wrapping a list of errors.
The implementation is lean as usual, no cruft attached.
Let’s review the changes.
First, an error is considered wrapping multiple errors when it has a method with the signature:
Unwrap() []error
All relevant functions for error checking error.Is()
and error.As()
has been updated, so they can be used to infer error presence in a chain even with multiple errors (neat!).
Formatting function has been updated too, so is possible to write:
fmt.Errorf("%w and %w", err1, err2)
Last but not least, we get a function to wrap multiple errors in a single one, errors.Join()
.
It seems all the needed building blocks are there, so let’s look at how to use them.
Caveats
error
does not implement Unwrap() []error
The first thing to notice here is that the builtin error
interface, does not mention Unwrap() []error
at all (docs), so is not possible to run Unwrap()
on errors joined with errors.Join()
or fmt.Errorf
.
This is a bit confusing at first, as you can join errors very easily but then is not possible to unwrap them.
errors.Unwrap()
will return nil
for multiple errors
As specified in the proposal, errors.Unwrap()
will returns nil
if the Unwrap
method returns []error
.
This decision prevent this change to break compatibility with previous Golang versions, but is a caveat to keep in mind.
Examples
Using error
s
err1 := errors.New("err1")
err2 := errors.New("err2")
errs := errors.Join(err1, err2)
errs
will be report true to errors.Is
for both err1
and err2
.
But errs
does not implement .Unwrap()
, so there is no way to unwrap this error and reuse single errors.
Formatting error
s
err1 := errors.New("err1")
err2 := errors.New("err2")
errs := fmt.Errorf("%w | %w", err1, err2)
As in the previous example, errs
will be report true to errors.Is
for both err1
and err2
, but does not implement .Unwrap()
.
Custom error
If we build a custom type implementing error
and the new Unwrap()
function, we can range over errors, as I initially expected.
type CustomError struct {
msg string
errs []error
}
func (f CustomError) Error() string { return f.msg }
func (e CustomError) Unwrap() []error {
return e.errs
}
...
errs := CustomError{"My Error", []error{err1, err2}}
if errors.Is(errs, err1) {
fmt.Println(" err is err1")
}
if errors.Is(errs, err2) {
fmt.Println(" err is err2")
}
for _, e := range errs.Unwrap() {
fmt.Printf(" unwrapped err: %s\n", e)
}
This is great, and I find it as a nice improvement over what I’m doing in my tool endorama/devid
.
Admittedly the solution was decent, using 2 return values ([]err, err)
where the usual error would signal the possibility for other errors to be present. But was lacking integrated support for checking the error chain, so that will make a great improvement!