multierr

package module
v1.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 29, 2023 License: MIT Imports: 7 Imported by: 4,116

README

multierr GoDoc Build Status Coverage Status

multierr allows combining one or more Go errors together.

Features

  • Idiomatic: multierr follows best practices in Go, and keeps your code idiomatic.
    • It keeps the underlying error type hidden, allowing you to deal in error values exclusively.
    • It provides APIs to safely append into an error from a defer statement.
  • Performant: multierr is optimized for performance:
    • It avoids allocations where possible.
    • It utilizes slice resizing semantics to optimize common cases like appending into the same error object from a loop.
  • Interoperable: multierr interoperates with the Go standard library's error APIs seamlessly:
    • The errors.Is and errors.As functions just work.
  • Lightweight: multierr comes with virtually no dependencies.

Installation

go get -u go.uber.org/multierr@latest

Status

Stable: No breaking changes will be made before 2.0.


Released under the MIT License.

Documentation

Overview

Package multierr allows combining one or more errors together.

Overview

Errors can be combined with the use of the Combine function.

multierr.Combine(
	reader.Close(),
	writer.Close(),
	conn.Close(),
)

If only two errors are being combined, the Append function may be used instead.

err = multierr.Append(reader.Close(), writer.Close())

The underlying list of errors for a returned error object may be retrieved with the Errors function.

errors := multierr.Errors(err)
if len(errors) > 0 {
	fmt.Println("The following errors occurred:", errors)
}

Appending from a loop

You sometimes need to append into an error from a loop.

var err error
for _, item := range items {
	err = multierr.Append(err, process(item))
}

Cases like this may require knowledge of whether an individual instance failed. This usually requires introduction of a new variable.

var err error
for _, item := range items {
	if perr := process(item); perr != nil {
		log.Warn("skipping item", item)
		err = multierr.Append(err, perr)
	}
}

multierr includes AppendInto to simplify cases like this.

var err error
for _, item := range items {
	if multierr.AppendInto(&err, process(item)) {
		log.Warn("skipping item", item)
	}
}

This will append the error into the err variable, and return true if that individual error was non-nil.

See AppendInto for more information.

Deferred Functions

Go makes it possible to modify the return value of a function in a defer block if the function was using named returns. This makes it possible to record resource cleanup failures from deferred blocks.

func sendRequest(req Request) (err error) {
	conn, err := openConnection()
	if err != nil {
		return err
	}
	defer func() {
		err = multierr.Append(err, conn.Close())
	}()
	// ...
}

multierr provides the Invoker type and AppendInvoke function to make cases like the above simpler and obviate the need for a closure. The following is roughly equivalent to the example above.

func sendRequest(req Request) (err error) {
	conn, err := openConnection()
	if err != nil {
		return err
	}
	defer multierr.AppendInvoke(&err, multierr.Close(conn))
	// ...
}

See AppendInvoke and Invoker for more information.

NOTE: If you're modifying an error from inside a defer, you MUST use a named return value for that function.

Advanced Usage

Errors returned by Combine and Append MAY implement the following interface.

type errorGroup interface {
	// Returns a slice containing the underlying list of errors.
	//
	// This slice MUST NOT be modified by the caller.
	Errors() []error
}

Note that if you need access to list of errors behind a multierr error, you should prefer using the Errors function. That said, if you need cheap read-only access to the underlying errors slice, you can attempt to cast the error to this interface. You MUST handle the failure case gracefully because errors returned by Combine and Append are not guaranteed to implement this interface.

var errors []error
group, ok := err.(errorGroup)
if ok {
	errors = group.Errors()
} else {
	errors = []error{err}
}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Append

func Append(left error, right error) error

Append appends the given errors together. Either value may be nil.

This function is a specialization of Combine for the common case where there are only two errors.

err = multierr.Append(reader.Close(), writer.Close())

The following pattern may also be used to record failure of deferred operations without losing information about the original error.

func doSomething(..) (err error) {
	f := acquireResource()
	defer func() {
		err = multierr.Append(err, f.Close())
	}()

Note that the variable MUST be a named return to append an error to it from the defer statement. See also AppendInvoke.

Example
package main

import (
	"errors"
	"fmt"

	"go.uber.org/multierr"
)

func main() {
	var err error
	err = multierr.Append(err, errors.New("call 1 failed"))
	err = multierr.Append(err, errors.New("call 2 failed"))
	fmt.Println(err)
}
Output:

call 1 failed; call 2 failed

func AppendFunc added in v1.9.0

func AppendFunc(into *error, fn func() error)

AppendFunc is a shorthand for AppendInvoke. It allows using function or method value directly without having to wrap it into an Invoker interface.

func doSomething(...) (err error) {
	w, err := startWorker(...)
	if err != nil {
		return err
	}

	// multierr will call w.Stop() when this function returns and
	// if the operation fails, it appends its error into the
	// returned error.
	defer multierr.AppendFunc(&err, w.Stop)
}

func AppendInto added in v1.4.0

func AppendInto(into *error, err error) (errored bool)

AppendInto appends an error into the destination of an error pointer and returns whether the error being appended was non-nil.

var err error
multierr.AppendInto(&err, r.Close())
multierr.AppendInto(&err, w.Close())

The above is equivalent to,

err := multierr.Append(r.Close(), w.Close())

As AppendInto reports whether the provided error was non-nil, it may be used to build a multierr error in a loop more ergonomically. For example:

var err error
for line := range lines {
	var item Item
	if multierr.AppendInto(&err, parse(line, &item)) {
		continue
	}
	items = append(items, item)
}

Compare this with a version that relies solely on Append:

var err error
for line := range lines {
	var item Item
	if parseErr := parse(line, &item); parseErr != nil {
		err = multierr.Append(err, parseErr)
		continue
	}
	items = append(items, item)
}
Example
package main

import (
	"errors"
	"fmt"

	"go.uber.org/multierr"
)

func main() {
	var err error

	if multierr.AppendInto(&err, errors.New("foo")) {
		fmt.Println("call 1 failed")
	}

	if multierr.AppendInto(&err, nil) {
		fmt.Println("call 2 failed")
	}

	if multierr.AppendInto(&err, errors.New("baz")) {
		fmt.Println("call 3 failed")
	}

	fmt.Println(err)
}
Output:

call 1 failed
call 3 failed
foo; baz

func AppendInvoke added in v1.7.0

func AppendInvoke(into *error, invoker Invoker)

AppendInvoke appends the result of calling the given Invoker into the provided error pointer. Use it with named returns to safely defer invocation of fallible operations until a function returns, and capture the resulting errors.

func doSomething(...) (err error) {
	// ...
	f, err := openFile(..)
	if err != nil {
		return err
	}

	// multierr will call f.Close() when this function returns and
	// if the operation fails, its append its error into the
	// returned error.
	defer multierr.AppendInvoke(&err, multierr.Close(f))

	scanner := bufio.NewScanner(f)
	// Similarly, this scheduled scanner.Err to be called and
	// inspected when the function returns and append its error
	// into the returned error.
	defer multierr.AppendInvoke(&err, multierr.Invoke(scanner.Err))

	// ...
}

NOTE: If used with a defer, the error variable MUST be a named return.

Without defer, AppendInvoke behaves exactly like AppendInto.

err := // ...
multierr.AppendInvoke(&err, mutltierr.Invoke(foo))

// ...is roughly equivalent to...

err := // ...
multierr.AppendInto(&err, foo())

The advantage of the indirection introduced by Invoker is to make it easy to defer the invocation of a function. Without this indirection, the invoked function will be evaluated at the time of the defer block rather than when the function returns.

// BAD: This is likely not what the caller intended. This will evaluate
// foo() right away and append its result into the error when the
// function returns.
defer multierr.AppendInto(&err, foo())

// GOOD: This will defer invocation of foo unutil the function returns.
defer multierr.AppendInvoke(&err, multierr.Invoke(foo))

multierr provides a few Invoker implementations out of the box for convenience. See Invoker for more information.

Example
package main

import (
	"fmt"
	"log"
	"os"
	"path/filepath"

	"go.uber.org/multierr"
)

func main() {
	if err := run(); err != nil {
		log.Fatal(err)
	}
}

func run() (err error) {
	dir, err := os.MkdirTemp("", "multierr")
	// We create a temporary directory and defer its deletion when this
	// function returns.
	//
	// If we failed to delete the temporary directory, we append its
	// failure into the returned value with multierr.AppendInvoke.
	//
	// This uses a custom invoker that we implement below.
	defer multierr.AppendInvoke(&err, RemoveAll(dir))

	path := filepath.Join(dir, "example.txt")
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	// Similarly, we defer closing the open file when the function returns,
	// and appends its failure, if any, into the returned error.
	//
	// This uses the multierr.Close invoker included in multierr.
	defer multierr.AppendInvoke(&err, multierr.Close(f))

	if _, err := fmt.Fprintln(f, "hello"); err != nil {
		return err
	}

	return nil
}

// RemoveAll is a multierr.Invoker that deletes the provided directory and all
// of its contents.
type RemoveAll string

func (r RemoveAll) Invoke() error {
	return os.RemoveAll(string(r))
}
Output:

func Combine

func Combine(errors ...error) error

Combine combines the passed errors into a single error.

If zero arguments were passed or if all items are nil, a nil error is returned.

Combine(nil, nil)  // == nil

If only a single error was passed, it is returned as-is.

Combine(err)  // == err

Combine skips over nil arguments so this function may be used to combine together errors from operations that fail independently of each other.

multierr.Combine(
	reader.Close(),
	writer.Close(),
	pipe.Close(),
)

If any of the passed errors is a multierr error, it will be flattened along with the other errors.

multierr.Combine(multierr.Combine(err1, err2), err3)
// is the same as
multierr.Combine(err1, err2, err3)

The returned error formats into a readable multi-line error message if formatted with %+v.

fmt.Sprintf("%+v", multierr.Combine(err1, err2))
Example
package main

import (
	"errors"
	"fmt"

	"go.uber.org/multierr"
)

func main() {
	err := multierr.Combine(
		errors.New("call 1 failed"),
		nil, // successful request
		errors.New("call 3 failed"),
		nil, // successful request
		errors.New("call 5 failed"),
	)
	fmt.Printf("%+v", err)
}
Output:

the following errors occurred:
 -  call 1 failed
 -  call 3 failed
 -  call 5 failed

func Errors added in v1.1.0

func Errors(err error) []error

Errors returns a slice containing zero or more errors that the supplied error is composed of. If the error is nil, a nil slice is returned.

err := multierr.Append(r.Close(), w.Close())
errors := multierr.Errors(err)

If the error is not composed of other errors, the returned slice contains just the error that was passed in.

Callers of this function are free to modify the returned slice.

Example
package main

import (
	"errors"
	"fmt"

	"go.uber.org/multierr"
)

func main() {
	err := multierr.Combine(
		nil, // successful request
		errors.New("call 2 failed"),
		errors.New("call 3 failed"),
	)
	err = multierr.Append(err, nil) // successful request
	err = multierr.Append(err, errors.New("call 5 failed"))

	errors := multierr.Errors(err)
	for _, err := range errors {
		fmt.Println(err)
	}
}
Output:

call 2 failed
call 3 failed
call 5 failed

func Every added in v1.11.0

func Every(err error, target error) bool

Every compares every error in the given err against the given target error using errors.Is, and returns true only if every comparison returned true.

Types

type Invoke added in v1.7.0

type Invoke func() error

Invoke wraps a function which may fail with an error to match the Invoker interface. Use it to supply functions matching this signature to AppendInvoke.

For example,

func processReader(r io.Reader) (err error) {
	scanner := bufio.NewScanner(r)
	defer multierr.AppendInvoke(&err, multierr.Invoke(scanner.Err))
	for scanner.Scan() {
		// ...
	}
	// ...
}

In this example, the following line will construct the Invoker right away, but defer the invocation of scanner.Err() until the function returns.

defer multierr.AppendInvoke(&err, multierr.Invoke(scanner.Err))

Note that the error you're appending to from the defer statement MUST be a named return.

func (Invoke) Invoke added in v1.7.0

func (i Invoke) Invoke() error

Invoke calls the supplied function and returns its result.

type Invoker added in v1.7.0

type Invoker interface {
	Invoke() error
}

Invoker is an operation that may fail with an error. Use it with AppendInvoke to append the result of calling the function into an error. This allows you to conveniently defer capture of failing operations.

See also, Close and Invoke.

func Close added in v1.7.0

func Close(closer io.Closer) Invoker

Close builds an Invoker that closes the provided io.Closer. Use it with AppendInvoke to close io.Closers and append their results into an error.

For example,

func processFile(path string) (err error) {
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer multierr.AppendInvoke(&err, multierr.Close(f))
	return processReader(f)
}

In this example, multierr.Close will construct the Invoker right away, but defer the invocation of f.Close until the function returns.

defer multierr.AppendInvoke(&err, multierr.Close(f))

Note that the error you're appending to from the defer statement MUST be a named return.

Example
package main

import (
	"errors"
	"fmt"
	"io"

	"go.uber.org/multierr"
)

type fakeCloser func() error

func (f fakeCloser) Close() error {
	return f()
}

func FakeCloser(err error) io.Closer {
	return fakeCloser(func() error {
		return err
	})
}

func main() {
	var err error

	closer := FakeCloser(errors.New("foo"))

	defer func() {
		fmt.Println(err)
	}()
	defer multierr.AppendInvoke(&err, multierr.Close(closer))

	fmt.Println("Hello, World")

}
Output:

Hello, World
foo

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL