Skip to content

Commit 293a3c8

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 657789a commit 293a3c8

24 files changed

+1568
-54
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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ func decodeLeaf(ctx context.Context, enc *errorspb.EncodedErrorLeaf) error {
6666
}
6767
}
6868

69+
if len(enc.Causes) > 0 {
70+
causes := make([]error, len(enc.Causes))
71+
for i, e := range enc.Causes {
72+
causes[i] = DecodeError(ctx, *e)
73+
}
74+
leaf := &opaqueLeafCauses{
75+
causes: causes,
76+
}
77+
leaf.msg = enc.Message
78+
leaf.details = enc.Details
79+
return leaf
80+
}
81+
6982
// No decoder and no error type: we'll keep what we received and
7083
// make it ready to re-encode exactly (if the error leaves over the
7184
// network again).

errbase/encode.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,20 @@ func EncodeError(ctx context.Context, err error) EncodedError {
3333
if cause := UnwrapOnce(err); cause != nil {
3434
return encodeWrapper(ctx, err, cause)
3535
}
36-
// Not a causer.
37-
return encodeLeaf(ctx, err)
36+
return encodeLeaf(ctx, err, UnwrapMulti(err))
3837
}
3938

4039
// encodeLeaf encodes a leaf error.
41-
func encodeLeaf(ctx context.Context, err error) EncodedError {
40+
func encodeLeaf(ctx context.Context, err error, causes []error) EncodedError {
4241
var msg string
4342
var details errorspb.EncodedErrorDetails
4443

4544
if e, ok := err.(*opaqueLeaf); ok {
4645
msg = e.msg
4746
details = e.details
47+
} else if e, ok := err.(*opaqueLeafCauses); ok {
48+
msg = e.msg
49+
details = e.details
4850
} else {
4951
details.OriginalTypeName, details.ErrorTypeMark.FamilyName, details.ErrorTypeMark.Extension = getTypeDetails(err, false /*onlyFamily*/)
5052

@@ -74,11 +76,21 @@ func encodeLeaf(ctx context.Context, err error) EncodedError {
7476
details.FullDetails = encodeAsAny(ctx, err, payload)
7577
}
7678

79+
var cs []*EncodedError
80+
if len(causes) > 0 {
81+
cs = make([]*EncodedError, len(causes))
82+
for i, ee := range causes {
83+
ee := EncodeError(ctx, ee)
84+
cs[i] = &ee
85+
}
86+
}
87+
7788
return EncodedError{
7889
Error: &errorspb.EncodedError_Leaf{
7990
Leaf: &errorspb.EncodedErrorLeaf{
8091
Message: msg,
8192
Details: details,
93+
Causes: cs,
8294
},
8395
},
8496
}
@@ -207,6 +219,8 @@ func getTypeDetails(
207219
switch t := err.(type) {
208220
case *opaqueLeaf:
209221
return t.details.OriginalTypeName, t.details.ErrorTypeMark.FamilyName, t.details.ErrorTypeMark.Extension
222+
case *opaqueLeafCauses:
223+
return t.details.OriginalTypeName, t.details.ErrorTypeMark.FamilyName, t.details.ErrorTypeMark.Extension
210224
case *opaqueWrapper:
211225
return t.details.OriginalTypeName, t.details.ErrorTypeMark.FamilyName, t.details.ErrorTypeMark.Extension
212226
}

errbase/opaque.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,30 @@ type opaqueLeaf struct {
3030
details errorspb.EncodedErrorDetails
3131
}
3232

33+
// opaqueLeafCauses is used when receiving an unknown multi-cause
34+
// wrapper type. Its important property is that if it is communicated
35+
// back to some network system that _does_ know about the type, the
36+
// original object can be restored. We encode multi-cause errors as
37+
// leaf nodes over the network, in order to support backwards
38+
// compatibility with existing single-cause wrapper messages.
39+
//
40+
// This struct *must* be initialized with a non-nil causes value in
41+
// order to comply with go stdlib expectations for `Unwrap()`.
42+
type opaqueLeafCauses struct {
43+
opaqueLeaf
44+
causes []error
45+
}
46+
3347
var _ error = (*opaqueLeaf)(nil)
3448
var _ SafeDetailer = (*opaqueLeaf)(nil)
3549
var _ fmt.Formatter = (*opaqueLeaf)(nil)
3650
var _ SafeFormatter = (*opaqueLeaf)(nil)
3751

52+
var _ error = (*opaqueLeafCauses)(nil)
53+
var _ SafeDetailer = (*opaqueLeafCauses)(nil)
54+
var _ fmt.Formatter = (*opaqueLeafCauses)(nil)
55+
var _ SafeFormatter = (*opaqueLeafCauses)(nil)
56+
3857
// opaqueWrapper is used when receiving an unknown wrapper type.
3958
// Its important property is that if it is communicated
4059
// back to some network system that _does_ know about
@@ -70,8 +89,12 @@ func (e *opaqueWrapper) Unwrap() error { return e.cause }
7089
func (e *opaqueLeaf) SafeDetails() []string { return e.details.ReportablePayload }
7190
func (e *opaqueWrapper) SafeDetails() []string { return e.details.ReportablePayload }
7291

73-
func (e *opaqueLeaf) Format(s fmt.State, verb rune) { FormatError(e, s, verb) }
74-
func (e *opaqueWrapper) Format(s fmt.State, verb rune) { FormatError(e, s, verb) }
92+
func (e *opaqueLeaf) Format(s fmt.State, verb rune) { FormatError(e, s, verb) }
93+
func (e *opaqueLeafCauses) Format(s fmt.State, verb rune) { FormatError(e, s, verb) }
94+
func (e *opaqueWrapper) Format(s fmt.State, verb rune) { FormatError(e, s, verb) }
95+
96+
// opaqueLeafCauses is a multi-cause wrapper
97+
func (e *opaqueLeafCauses) Unwrap() []error { return e.causes }
7598

7699
func (e *opaqueLeaf) SafeFormatError(p Printer) (next error) {
77100
p.Print(e.msg)

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() }

0 commit comments

Comments
 (0)