Open In App

Joining Errors in Golang

Last Updated : 09 Oct, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

The stdlib "errors" package in Go supports joining multiple errors in the addition to more commin %w wrapping.

Joining errors

You can join multiple errors in two ways. They have slightly different semantics under the hood (look at the Appendix section if you care), but they both work in a similar way.

1. By using multiple %w verbs on the same error

Go
var (
    ErrRelayOrientation = errors.New("bad relay orientation")
    ErrCosmicRayBitflip = errors.New("cosmic ray bitflip")
    ErrStickyPlunger    = errors.New("sticky sensor plunger")
)

err1 := fmt.Errorf("G-switch failed: %w %w %w", ErrRelayOrientation, ErrCosmicRayBitflip, ErrStickyPlunger)

// 2009/11/10 23:00:00 G-switch failed: bad relay orientation cosmic ray bitflip sticky sensor plunger
log.Fatal(err1)

2. The second one uses the errors.Join function introduced in Go 1.20.

The function takes in a variadic error argument, discards any nil values, and joins the rest of the provided errors. The message is formatted by joining the strings obtained by calling each argument’s Error() method, separated by a newline.

Go
err2 :=  errors.Join(
    ErrRelayOrientation,
    ErrCosmicRayBitflip,
    ErrStickyPlunger,
)

// 2009/11/10 23:00:00 bad relay orientation
// cosmic ray bitflip
// sticky sensor plunger
log.Fatal(err2)

How to use them?

By now, we’ve seen the two ways that Go supports error wrapping, direct wrapping and joined errors.

Both variants ultimately form a tree of errors. The most common ways to inspect that tree are the errors.Is and errors.As functions. Both of these examine the tree in a pre-order, depth-first traversal by successively unwrapping every node found.

Go
func Is(err, target error) bool
func As(err error, target any) bool

The errors.Is function examines the input error’s tree, looking for a leaf that matches the target argument and reports if it finds a match. In our case, this can look for a leaf node that matches a specific joined error.

Go
ok := errors.Is(err1, ErrStickyPlunger)
fmt.Println(ok) // true

On the other hand, errors.As examines the input error’s tree, looking for a leaf that can be assigned to the type of the target argument. Think of it as an analog to json.Unmarshal.

Go
var engineErr *EngineError
ok = errors.As(err2, &engineErr)
fmt.Println(ok) // false


So, to summarize:

  • errors.Is checks if a specific error is part of the error tree
  • errors.As checks if the error tree contains an error that can be assigned to a target type

The catch

So far so good! We can use these two types of error wrapping to form a tree. But let’s say we wanted to inspect that tree in a more manual way on another part of the codebase. The errors.Unwrap function allows you to get direct wrapped errors.

But there’s a slight complication here. Let’s try to call errors.Unwrap() directly on any of the two joined errors created above.

Go
fmt.Println(errors.Unwrap(err1)) // nil
fmt.Println(errors.Unwrap(err2)) // nil

So, why nil?! What’s going on? How can I get the original errors slice and inspect it? Turns out, that the two ‘varieties’ implement a different Unwrap method.

Go
Unwrap() error
Unwrap() []error


The documentation of errors.Unwrap method clearly states that it only calls the first one and does not unwrap errors returned by Join. There have been multiple discussions on golang/go about allowing a more straightforward way to unwrap joined errors, but there has been no consensus.

The way to achieve it right now is to either use errors.As or an inline interface cast to get access to the second Unwrap implementation.

Go
var joinedErrors interface{ Unwrap() []error }


// You can use errors.As to make sure that the alternate Unwrap() implementation is available
if errors.As(err1, &joinedErrors) {
	for _, e := range joinedErrors.Unwrap() {
		fmt.Println("-", e)
	}
}

// Or do it more directly with an inline cast
if uw, ok := err2.(interface{ Unwrap() []error }); ok {
	for _, e := range uw.Unwrap() {
		fmt.Println("~", e)
	}
}

So, it’s an extra little step, but with either of these techniques you’ll be able to retrieve the original slice of errors and manually traverse the error tree.


Next Article

Similar Reads