Skip to content

Commit 8514096

Browse files
committed
support multi-cause errors in go 1.20
Go 1.20 introduces the idea of an error with multiple causes instead of a single chain. This commit updates the errors library to properly encode, decode, and format these error types. For encoding and decoding we use the existing `EncodedLeaf` type and embellish it with a `causes` field. This is done in order to keep the encoding/decoding backwards compatible. `EncodedLeaf` types containing multiple causes when decided by earlier versions will simply see an opaque leaf with a message inside. The reason the `EncodedWrapper` is not used here is because the wrapper already contains a mandatory single `cause` field that we cannot fill with the multi-errors. A new type cannot be used because it would not be decodable by older versions of this library.
1 parent 81026ce commit 8514096

26 files changed

+2111
-52
lines changed

errbase/adapters_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,33 @@ func TestAdaptGoSingleWrapErr(t *testing.T) {
6464
tt := testutils.T{T: t}
6565
// The library preserves the cause. It's not possible to preserve the fmt
6666
// string.
67+
tt.CheckEqual(newErr.Error(), origErr.Error())
68+
tt.CheckContains(newErr.Error(), "hello")
69+
}
70+
71+
func TestAdaptBaseGoJoinErr(t *testing.T) {
72+
origErr := goErr.Join(goErr.New("hello"), goErr.New("world"))
73+
t.Logf("start err: %# v", pretty.Formatter(origErr))
74+
75+
newErr := network(t, origErr)
76+
77+
tt := testutils.T{T: t}
78+
// The library preserves the error message.
79+
tt.CheckEqual(newErr.Error(), origErr.Error())
80+
81+
}
82+
83+
func TestAdaptGoMultiWrapErr(t *testing.T) {
84+
origErr := fmt.Errorf("an error %w and also %w", goErr.New("hello"), goErr.New("world"))
85+
t.Logf("start err: %# v", pretty.Formatter(origErr))
86+
87+
newErr := network(t, origErr)
88+
89+
tt := testutils.T{T: t}
90+
// The library preserves the causes. It's not possible to preserve the fmt string.
91+
tt.CheckEqual(newErr.Error(), origErr.Error())
6792
tt.CheckContains(newErr.Error(), "hello")
93+
tt.CheckContains(newErr.Error(), "world")
6894
}
6995

7096
func TestAdaptPkgWithMessage(t *testing.T) {

errbase/decode.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,18 @@ func decodeLeaf(ctx context.Context, enc *errorspb.EncodedErrorLeaf) error {
6666
}
6767
}
6868

69+
causes := make([]error, len(enc.Causes))
70+
for i, e := range enc.Causes {
71+
causes[i] = DecodeError(ctx, *e)
72+
}
73+
6974
// No decoder and no error type: we'll keep what we received and
7075
// make it ready to re-encode exactly (if the error leaves over the
7176
// network again).
7277
return &opaqueLeaf{
7378
msg: enc.Message,
7479
details: enc.Details,
80+
causes: causes,
7581
}
7682
}
7783

errbase/encode.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,15 @@ func EncodeError(ctx context.Context, err error) EncodedError {
3333
if cause := UnwrapOnce(err); cause != nil {
3434
return encodeWrapper(ctx, err, cause)
3535
}
36+
if causes := UnwrapMulti(err); causes != nil {
37+
return encodeLeaf(ctx, err, causes)
38+
}
3639
// Not a causer.
37-
return encodeLeaf(ctx, err)
40+
return encodeLeaf(ctx, err, nil)
3841
}
3942

4043
// encodeLeaf encodes a leaf error.
41-
func encodeLeaf(ctx context.Context, err error) EncodedError {
44+
func encodeLeaf(ctx context.Context, err error, causes []error) EncodedError {
4245
var msg string
4346
var details errorspb.EncodedErrorDetails
4447

@@ -74,11 +77,18 @@ func encodeLeaf(ctx context.Context, err error) EncodedError {
7477
details.FullDetails = encodeAsAny(ctx, err, payload)
7578
}
7679

80+
cs := make([]*EncodedError, len(causes))
81+
for i, ee := range causes {
82+
ee := EncodeError(ctx, ee)
83+
cs[i] = &ee
84+
}
85+
7786
return EncodedError{
7887
Error: &errorspb.EncodedError_Leaf{
7988
Leaf: &errorspb.EncodedErrorLeaf{
8089
Message: msg,
8190
Details: details,
91+
Causes: cs,
8292
},
8393
},
8494
}
@@ -120,6 +130,7 @@ func encodeWrapper(ctx context.Context, err, cause error) EncodedError {
120130
if e, ok := err.(*opaqueWrapper); ok {
121131
msg, ownError = extractPrefix(err, cause)
122132
details = e.details
133+
ownError = e.ownsErrorString
123134
} else {
124135
details.OriginalTypeName, details.ErrorTypeMark.FamilyName, details.ErrorTypeMark.Extension = getTypeDetails(err, false /*onlyFamily*/)
125136

errbase/opaque.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
type opaqueLeaf struct {
2929
msg string
3030
details errorspb.EncodedErrorDetails
31+
causes []error
3132
}
3233

3334
var _ error = (*opaqueLeaf)(nil)
@@ -73,6 +74,9 @@ func (e *opaqueWrapper) SafeDetails() []string { return e.details.ReportablePayl
7374
func (e *opaqueLeaf) Format(s fmt.State, verb rune) { FormatError(e, s, verb) }
7475
func (e *opaqueWrapper) Format(s fmt.State, verb rune) { FormatError(e, s, verb) }
7576

77+
// opaque leaf can be a multi-wrapper
78+
func (e *opaqueLeaf) Unwrap() []error { return e.causes }
79+
7680
func (e *opaqueLeaf) SafeFormatError(p Printer) (next error) {
7781
p.Print(e.msg)
7882
if p.Detail() {

errbase/unwrap.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ package errbase
2727
// It supports both errors implementing causer (`Cause()` method, from
2828
// github.com/pkg/errors) and `Wrapper` (`Unwrap()` method, from the
2929
// Go 2 error proposal).
30+
//
31+
// UnwrapOnce treats multi-errors (those implementing the
32+
// `Unwrap() []error` interface as leaf-nodes since they cannot
33+
// reasonably be iterated through to a single cause. These errors
34+
// are typically constructed as a result of `fmt.Errorf` which results
35+
// in a `wrapErrors` instance that contains an interpolated error
36+
// string along with a list of causes.
37+
//
38+
// The go stdlib does not define output on `Unwrap()` for a multi-cause
39+
// error, so we default to nil here.
3040
func UnwrapOnce(err error) (cause error) {
3141
switch e := err.(type) {
3242
case interface{ Cause() error }:
@@ -39,6 +49,7 @@ func UnwrapOnce(err error) (cause error) {
3949

4050
// UnwrapAll accesses the root cause object of the error.
4151
// If the error has no cause (leaf error), it is returned directly.
52+
// UnwrapAll treats multi-errors as leaf nodes.
4253
func UnwrapAll(err error) error {
4354
for {
4455
if cause := UnwrapOnce(err); cause != nil {
@@ -49,3 +60,12 @@ func UnwrapAll(err error) error {
4960
}
5061
return err
5162
}
63+
64+
// UnwrapMulti access the slice of causes that an error contains, if it is a
65+
// multi-error.
66+
func UnwrapMulti(err error) []error {
67+
if me, ok := err.(interface{ Unwrap() []error }); ok {
68+
return me.Unwrap()
69+
}
70+
return nil
71+
}

errbase/unwrap_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package errbase_test
1616

1717
import (
1818
"errors"
19+
"fmt"
1920
"testing"
2021

2122
"github.com/cockroachdb/errors/errbase"
@@ -58,6 +59,18 @@ func TestMixedErrorWrapping(t *testing.T) {
5859
tt.CheckEqual(errbase.UnwrapAll(err3), err)
5960
}
6061

62+
func TestMultiErrorUnwrap(t *testing.T) {
63+
tt := testutils.T{T: t}
64+
65+
err := errors.New("hello")
66+
err2 := pkgErr.WithMessage(err, "woo")
67+
err3 := fmt.Errorf("%w %w", err, err2)
68+
69+
tt.CheckEqual(errbase.UnwrapOnce(err3), nil)
70+
tt.CheckEqual(errbase.UnwrapAll(err3), err3)
71+
tt.CheckDeepEqual(errbase.UnwrapMulti(err3), []error{err, err2})
72+
}
73+
6174
type myWrapper struct{ cause error }
6275

6376
func (w *myWrapper) Error() string { return w.cause.Error() }

errorspb/errors.pb.go

Lines changed: 104 additions & 44 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)