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 an Unwrap method that returns a []error.
The errors.Is and errors.As functions have been updated to inspect multiply wrapped errors.
The fmt.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 function errors.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

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.

Playground code

Formatting error

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().

Playground code

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)
}

Playground code

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!