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:
errors.Is
and errors.As
allows checking specific error types at any level of the application, regardless of how deeply they are wrapped.<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:
ErrDatabaseConflict
, ErrCodeServiceExample
, ErrCodeHandlerExample
) are defined using errors.New()
err1
is created using fmt.Errorf()
, which combines all three errors using the %w
verb%w
in a single fmt.Errorf()
allows creating a chain of wrapped errorslog.Fatal()
, we see all three errors in one message, separated by spacesThis 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)
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.