Joe Davidson

Migrate from Hashicorps go-multierror to standard library multierror

goguidemultierror

A migration guide from github.com/hashicorp/go-multierror to the std library errors package.

In go1.20 support for "multierrors" was added.

TLDR

Why migrate? #

Migrating from community go modules to standard library implementations is almost always a no-brainer if it provides like for like functionality.

Packages in the core go standard library such as errors are covered by the go1.0 backwards compatibility guarantee [0].
Because of this they are also guaranteed to be supported with security fixes and other patches.

APIs #

go-multierror #

go-multierror follows a similar API to how slices are appended to in go.

Playground: https://go.dev/play/p/DrfOauIC1Ou

var result error

if err := step1(); err != nil {
result = multierror.Append(result, err)
}
if err := step2(); err != nil {
result = multierror.Append(result, err)
}
if result != nil {
fmt.Printf("some error: %v", result)
}

multierror.Append returns a *multierror.Error which can be further appended to. And if a multierror is appended to another, they will be flattened to make a flat list of errors.

errors.Join #

The standard library implementation is as a new function in the errors package.

func Join(errs ...error) error

And a new unexported interface.

interface { Unwrap() []error }

Playground: https://go.dev/play/p/D7gFVsBwIbg

err1 := step1()
err2 := step2()
err := errors.Join(err1, err2)
if err != nil {
return fmt.Error("some error: %w", err)
}

Differences #

Although at a high level the APIs are similar, there is one big difference which is that errors.Join does not flatten other "joined" errors.

Playground: https://go.dev/play/p/f0AGNCkHmC3

var result error

if err := step1(); err != nil {
result = multierror.Append(result, err)
}
if err := step2(); err != nil {
result = multierror.Append(result, err)
}
fmt.Printf("%#v", result)
// [err, err]

Playground: https://go.dev/play/p/k_90D8Yu8oY

var result error

if err := step1(); err != nil {
result = errors.Join(result, err)
}
if err := step2(); err != nil {
result = errors.Join(result, err)
}
result
// [[err], err]

Migrating #

Due to the above differences, when moving from go-multierror to errors you should really be calling errors.Join once unless you want a "tree" of errors.

Set number of errors #

If your code currently looks similar to this:

Playground: https://go.dev/play/p/CtqsNTYl6Jk

var result error

if err := step1(); err != nil {
result = multierror.Append(result, err)
}
if err := step2(); err != nil {
result = multierror.Append(result, err)
}
if result != nil {
// handle error
}

Then you should move to this:

Playground: https://go.dev/play/p/rRXX2SYbuBH

err1 := step1()
err2 := step2()
err := errors.Join(err1, err2)
if err != nil {
// handle error
}

or

Playground: https://go.dev/play/p/rI58PdSVHWj

err := errors.Join(
step1(),
step2(),
)
if err != nil {
// handle error
}

Loops #

If your code currently looks similar to this:

Playground: https://go.dev/play/p/HgEYsuiGivP

var result error

for _, s := range mySlice {
if err := process(s); err != nil {
result = multierror.Append(result, err)
}
}

if result != nil {
// handle error
}

Then you should move to this:

Playground: https://go.dev/play/p/uV7GUzauY31

var errs []error

for _, s := range mySlice {
if err := process(s); err != nil {
errs = append(errs, err)
}
}

err := errors.Join(errs...) // Join the errors once!
if err != nil {
// handle error
}