Unwrap returns a slice of errors rather than a single one. That is what
lets errors.Is match both a skyl sentinel and a wrapped standard error on the
same value.
Reference#
Returns
| State | Returns |
|---|---|
| Kind and cause both set | []error{Kind, cause} |
| Kind only | []error{Kind} |
| cause only | []error{cause} |
| neither | nil |
Caveats
errors.Isanderrors.Ashandle the slice form natively, so nothing in your code changes to take advantage of it.- The cause is set by adapters through
WithCausefor transport failures — a dial timeout, a TLS failure, a cancelled context. Cause()returns it directly if you need the value rather than a match.
Why a slice#
Deep diveFlattening the cause loses the information you need
A dial timeout, a DNS failure and a rejected certificate all arrive with
StatusCode == 0 and no sentinel. Flattening the cause into a message string
makes them indistinguishable — you get three different problems reported
identically, at exactly the moment you are trying to tell them apart.
With the multi-error Unwrap, precise questions work:
switch {
case errors.Is(err, context.DeadlineExceeded):
// We ran out of time.
case errors.Is(err, context.Canceled):
// The caller went away.
default:
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
// A transport-level timeout, distinct from our deadline.
}
}switch {
case errors.Is(err, context.DeadlineExceeded):
// We ran out of time.
case errors.Is(err, context.Canceled):
// The caller went away.
default:
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
// A transport-level timeout, distinct from our deadline.
}
}And the sentinel still matches on the same value, so
errors.Is(err, skyl.ErrServer) is unaffected.
Usage#
Matching both at once
// Both of these are true of the same error.
timedOut := errors.Is(err, context.DeadlineExceeded)
serverSide := errors.Is(err, skyl.ErrServer)// Both of these are true of the same error.
timedOut := errors.Is(err, context.DeadlineExceeded)
serverSide := errors.Is(err, skyl.ErrServer)Recognising a cancelled stream
if err := stream.Err(); err != nil {
if errors.Is(err, context.Canceled) {
return nil // the caller went away; not worth reporting
}
return err
}if err := stream.Err(); err != nil {
if errors.Is(err, context.Canceled) {
return nil // the caller went away; not worth reporting
}
return err
}Troubleshooting#
errors.Is(err, context.DeadlineExceeded) returns false
The adapter did not attach a cause — either it was not a transport failure, or
the error came from somewhere that had no cause to attach. Check
e.StatusCode: a non-zero status means the provider answered, so no context
error is involved.
I used to type-assert Unwrap() to error and it broke
Unwrap returns []error now. Use errors.Is and errors.As, which handle
both forms — direct calls to Unwrap were never the intended interface.