Error wrapping in Go
For the robust, an error is information; for the fragile, an error is an error.
—Nassim Nicholas Taleb, “The Bed of Procrustes: Philosophical and Practical Aphorisms”
This is Part 3 of a series about errors and error testing in Go:
- Testing errors in Go
- Comparing Go error values
- Error wrapping in Go
In Part 1 we talked about how to test simple errors, and in Part 2 we learned how to compare the values of those errors with what we expect.
In this final part, we’ll see how to overcome the limitations of
sentinel errors in Go by using error wrapping, and we’ll also
look at custom error types and how to distinguish different kinds of
errors using the errors.Is
and errors.As
functions.
Detecting sentinel
errors with errors.Is
In the previous part of this
series, we saw that for very simple cases, a fixed error value (known as
a sentinel error) is fine. And since all that matters about an
error like this is that it’s not nil
, that’s pretty easy to
test, or check for at runtime.
We don’t need to worry about what the specific error value is, because we don’t usually need to take different actions for different errors. Instead, we just report the error to the user and leave it up to them what to do.
What about when different errors should result in the code taking different actions, though? Even if most of the time this isn’t necessary, it’s worth asking how we should go about constructing distinct errors when that’s appropriate.
For example, suppose we’re writing a function that makes HTTP
requests to some API; let’s call it Request
. One of the
possible responses we can get is HTTP status 429 (Too Many Requests),
indicating that we are being ratelimited.
How could we indicate this in an error returned from our function? A sentinel value would be fine here:
var ErrRateLimit = errors.New("rate limit")
Then we can use it like this:
if resp.StatusCode == http.StatusTooManyRequests {
return ErrRateLimit
}
So, how can we test that the function indeed returns this error value when it gets a ratelimit response? First, we’ll need to create some local HTTP server that just responds to any request with the ratelimit status code:
func newRateLimitingServer() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
.WriteHeader(http.StatusTooManyRequests)
w}))
}
Great. Now we have a test server whose URL we can pass to
Request
, and the result should always be
ErrRateLimit
. But how can we check that result in
a test? The answer is to use errors.Is
:
if !errors.Is(err, req.ErrRateLimit) {
.Errorf("wrong error: %v", err)
t}
Wrapping sentinel errors with dynamic information
A sentinel error is pretty limited in what it can convey. It can’t contain any dynamic information that we’ve learned at run-time: it’s just a fixed string, and sometimes that’s fine, as in the ratelimit example.
But often we’d like to include some more dynamic information in the
error. In our store.Open
example from Part 1, that might be the full
err
value returned from os.Open
. How can we
incorporate that information into our sentinel error?
One way to do this is to take the sentinel value and wrap it
in a new error value that contains the dynamic information. For this, we
can use the fmt.Errorf
function, and the special format
verb %w
(for “wrap”):
var ErrUserNotFound = errors.New("user not found")
func FindUser(name string) (*User, error) {
, ok := userDB[name]
userif !ok {
return nil, fmt.Errorf("%q: %w", name, ErrUserNotFound)
}
return user, nil
}
The calling code, if it cares, can then use errors.Is
to
ask if the error result was originally
ErrUserNotFound
:
func TestFindUser_GivesErrUserNotFoundForBogusUser(t *testing.T) {
.Parallel()
t, err := user.FindUser("bogus user")
_if !errors.Is(err, user.ErrUserNotFound) {
.Errorf("wrong error: %v", err)
t}
}
Now we can see why using errors.Is
beats comparing
errors directly with the ==
operator. A direct comparison
wouldn’t succeed with a wrapped error. But errors.Is
can
inspect the error on a deeper level, to see what sentinel value is
hidden inside the wrapping.
Custom error types and
errors.As
In some older Go programs, written before error wrapping was introduced, you’ll see a custom error type used to achieve roughly the same goal. This is usually some struct type, for example, where the type name conveys what kind of error it is, and the struct fields convey the extra information:
type ErrUserNotFound struct {
string
User }
func (e ErrUserNotFound) Error() string {
return fmt.Sprintf("user %q not found", e.User)
}
And there’s a special function to check whether an error is one of
these named types. Instead of errors.Is
, we use
errors.As
:
if errors.As(err, &ErrUserNotFound{}) {
... // this is a 'UserNotFound' error
}
There’s no need to use custom error types anymore. In most situations
the best thing to do is to wrap a sentinel error using
fmt.Errorf
and %w
.