Hi! Today I would like to discuss a topic that often comes up in our daily work - error handling and wrapping, which is a critical aspect of developing reliable Go applications for several reasons:

<aside> 💡

Let's look at an example: in a web service, a low-level database error can be wrapped at the repository level, then at the service level, and finally at the HTTP handler level, where:

This is especially useful when developing microservices, where proper error handling and logging are critical for diagnosing problems in production.

The standard errors package in Go supports joining multiple errors in addition to the more common wrapping using %w.

I haven't seen this used often in practice; I think most people either refactor code to avoid multiple errors, return a slice of []error, or use uber-go/multierr. Let's look at this!

var (
    ErrDatabaseConflict   = errors.New("connection refused")
    ErrCodeServiceExample = errors.New("cannot get user profile")
    ErrCodeHandlerExample = errors.New("internal error")
)

err1 := fmt.Errorf("G-switch failed: %w %w %w", ErrDatabaseConflict, ErrCodeServiceExample, ErrCodeHandlerExample)

// 2009/11/10 23:00:00 G-switch failed: connection refused cannot get user profile internal error
log.Fatal(err1)

Let's break down this code in detail:

This is useful when you need to preserve information about multiple errors that occurred simultaneously and pass them up the call stack as a single error.

The second approach uses the errors.Join function, introduced in Go 1.20. The function takes a variable number of error arguments, discards all nil values, and joins the remaining provided errors. The message is formatted by joining the strings obtained by calling the Error() method of each argument, separated by a newline character.

err2 :=  errors.Join(
    ErrDatabaseConflict,
    ErrServiceCodeExample,
    ErrHandlerCodeExample,
)

// 2009/11/10 23:00:00 connection refused
// cannot get user profile
// internal error
log.Fatal(err2)

How to use them?

At this point, we've looked at two ways Go supports error wrapping: direct wrapping and joined errors.

Both variants ultimately form an error tree. The most common ways to check this tree are the errors.Is and errors.As functions. Both functions examine the tree in depth-first order, unwrapping each node as they go.