Skip to content

Commit 89c9f9e

Browse files
StevenACoffmandhartunian
authored andcommitted
Add Go 1.20 errors.Join support
This commit introduces a drop-in replacement of the go errors lib `Join` method which constructs a simple multi-cause error object. Some simple unit tests are added and `Join` wrappers are also integrated into the datadriven formatting test. Our existing format code for multi-cause errors is compatible with this new type which allows us to remove the custom formatter from the implementation. The public Join API contains wrappers that automatically attach stacktraces to the join errors. Signed-off-by: Steve Coffman <steve@khanacademy.org>
1 parent f0a2a69 commit 89c9f9e

16 files changed

+2449
-0
lines changed

errutil/utilities.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package errutil
1616

1717
import (
18+
"github.com/cockroachdb/errors/join"
1819
"github.com/cockroachdb/errors/secondary"
1920
"github.com/cockroachdb/errors/withstack"
2021
"github.com/cockroachdb/redact"
@@ -158,3 +159,10 @@ func WrapWithDepthf(depth int, err error, format string, args ...interface{}) er
158159
err = withstack.WithStackDepth(err, depth+1)
159160
return err
160161
}
162+
163+
// JoinWithDepth constructs a Join error with the provided list of
164+
// errors as arguments, and wraps it in a `WithStackDepth` to capture a
165+
// stacktrace alongside.
166+
func JoinWithDepth(depth int, errs ...error) error {
167+
return withstack.WithStackDepth(join.Join(errs...), depth+1)
168+
}

errutil_api.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,19 @@ func HandleAsAssertionFailureDepth(depth int, origErr error) error {
192192
// - it also supports recursing through causes with Cause().
193193
// - if it detects an API use error, its panic object is a valid error.
194194
func As(err error, target interface{}) bool { return errutil.As(err, target) }
195+
196+
// Join returns an error that wraps the given errors.
197+
// Any nil error values are discarded.
198+
// Join returns nil if errs contains no non-nil values.
199+
// The error formats as the concatenation of the strings obtained
200+
// by calling the Error method of each element of errs, with a newline
201+
// between each string. A stack trace is also retained.
202+
func Join(errs ...error) error {
203+
return errutil.JoinWithDepth(1, errs...)
204+
}
205+
206+
// JoinWithDepth is like Join but the depth at which the call stack is
207+
// captured can be specified.
208+
func JoinWithDepth(depth int, errs ...error) error {
209+
return errutil.JoinWithDepth(depth+1, errs...)
210+
}

errutil_api_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package errors_test
22

33
import (
44
"fmt"
5+
"strings"
56
"testing"
67

78
"github.com/cockroachdb/errors"
@@ -17,3 +18,14 @@ func TestUnwrap(t *testing.T) {
1718
// (per API documentation)
1819
tt.Check(errors.Unwrap(e) == nil)
1920
}
21+
22+
// More detailed testing of Join is in datadriven_test.go. Here we make
23+
// sure that the public API includes the stacktrace wrapper.
24+
func TestJoin(t *testing.T) {
25+
e := errors.Join(errors.New("abc123"), errors.New("def456"))
26+
printed := fmt.Sprintf("%+v", e)
27+
expected := `Error types: (1) *withstack.withStack (2) *join.joinError (3) *withstack.withStack (4) *errutil.leafError (5) *withstack.withStack (6) *errutil.leafError`
28+
if !strings.Contains(printed, expected) {
29+
t.Errorf("Expected: %s to contain: %s", printed, expected)
30+
}
31+
}

fmttests/datadriven_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/cockroachdb/errors/errutil"
3838
"github.com/cockroachdb/errors/hintdetail"
3939
"github.com/cockroachdb/errors/issuelink"
40+
"github.com/cockroachdb/errors/join"
4041
"github.com/cockroachdb/errors/report"
4142
"github.com/cockroachdb/errors/safedetails"
4243
"github.com/cockroachdb/errors/secondary"
@@ -276,6 +277,9 @@ var wrapCommands = map[string]commandFn{
276277
ctx = logtags.AddTag(ctx, "safe", redact.Safe(456))
277278
return contexttags.WithContextTags(err, ctx)
278279
},
280+
"join": func(err error, args []arg) error {
281+
return join.Join(err, errutil.New(strfy(args)))
282+
},
279283
}
280284

281285
var noPrefixWrappers = map[string]bool{
@@ -300,6 +304,7 @@ var noPrefixWrappers = map[string]bool{
300304
"stack": true,
301305
"tags": true,
302306
"telemetry": true,
307+
"join": true,
303308
}
304309

305310
var wrapOnlyExceptions = map[string]string{}
@@ -329,6 +334,7 @@ func init() {
329334
// means they don't match.
330335
"nofmt",
331336
"errorf",
337+
"join",
332338
} {
333339
wrapOnlyExceptions[v] = `
334340
accept %\+v via Formattable.*IRREGULAR: not same as %\+v

fmttests/testdata/format/wrap-fmt

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2251,6 +2251,197 @@ Type: "*fmttests.errFmt"
22512251
Title: "×"
22522252
(NO STACKTRACE)
22532253

2254+
run
2255+
fmt innerone innertwo
2256+
join outerthree outerfour
2257+
2258+
accept %\+v via Formattable.*IRREGULAR: not same as %\+v
2259+
accept %\#v via Formattable.*IRREGULAR: not same as %\#v
2260+
2261+
require (?s)
2262+
----
2263+
&join.joinError{
2264+
errs: {
2265+
&fmttests.errFmt{msg:"innerone\ninnertwo"},
2266+
&withstack.withStack{
2267+
cause: &errutil.leafError{msg:"outerthree\nouterfour"},
2268+
stack: &stack{...},
2269+
},
2270+
},
2271+
}
2272+
=====
2273+
===== non-redactable formats
2274+
=====
2275+
== %#v
2276+
&join.joinError{
2277+
errs: {
2278+
&fmttests.errFmt{msg:"innerone\ninnertwo"},
2279+
&withstack.withStack{
2280+
cause: &errutil.leafError{msg:"outerthree\nouterfour"},
2281+
stack: &stack{...},
2282+
},
2283+
},
2284+
}
2285+
== Error()
2286+
innerone
2287+
innertwo
2288+
outerthree
2289+
outerfour
2290+
== %v = Error(), good
2291+
== %s = Error(), good
2292+
== %q = quoted Error(), good
2293+
== %x = hex Error(), good
2294+
== %X = HEX Error(), good
2295+
== %+v
2296+
innerone
2297+
(1) innerone
2298+
| innertwo
2299+
| outerthree
2300+
| outerfour
2301+
Wraps: (2) attached stack trace
2302+
-- stack trace:
2303+
| github.com/cockroachdb/errors/fmttests.glob...funcNN...
2304+
| <tab><path>:<lineno>
2305+
| github.com/cockroachdb/errors/fmttests.TestDatadriven.func2.1
2306+
| <tab><path>:<lineno>
2307+
| github.com/cockroachdb/datadriven.runDirective.func1
2308+
| <tab><path>:<lineno>
2309+
| github.com/cockroachdb/datadriven.runDirective
2310+
| <tab><path>:<lineno>
2311+
| github.com/cockroachdb/datadriven.runDirectiveOrSubTest
2312+
| <tab><path>:<lineno>
2313+
| github.com/cockroachdb/datadriven.runTestInternal
2314+
| <tab><path>:<lineno>
2315+
| github.com/cockroachdb/datadriven.RunTest
2316+
| <tab><path>:<lineno>
2317+
| github.com/cockroachdb/errors/fmttests.TestDatadriven.func2
2318+
| <tab><path>:<lineno>
2319+
| github.com/cockroachdb/datadriven.Walk
2320+
| <tab><path>:<lineno>
2321+
| github.com/cockroachdb/datadriven.Walk.func1
2322+
| <tab><path>:<lineno>
2323+
| testing.tRunner
2324+
| <tab><path>:<lineno>
2325+
| runtime.goexit
2326+
| <tab><path>:<lineno>
2327+
└─ Wraps: (3) outerthree
2328+
| outerfour
2329+
Wraps: (4) innerone
2330+
| innertwo
2331+
| -- this is innerone
2332+
| innertwo's
2333+
| multi-line leaf payload
2334+
Error types: (1) *join.joinError (2) *withstack.withStack (3) *errutil.leafError (4) *fmttests.errFmt
2335+
== %#v via Formattable() = %#v, good
2336+
== %v via Formattable() = Error(), good
2337+
== %s via Formattable() = %v via Formattable(), good
2338+
== %q via Formattable() = quoted %v via Formattable(), good
2339+
== %+v via Formattable() == %+v, good
2340+
=====
2341+
===== redactable formats
2342+
=====
2343+
== printed via redact Print(), ok - congruent with %v
2344+
‹innerone›
2345+
‹innertwo›
2346+
outerthree
2347+
outerfour
2348+
== printed via redact Printf() %v = Print(), good
2349+
== printed via redact Printf() %s = Print(), good
2350+
== printed via redact Printf() %q, refused - good
2351+
== printed via redact Printf() %x, refused - good
2352+
== printed via redact Printf() %X, refused - good
2353+
== printed via redact Printf() %+v, ok - congruent with %+v
2354+
‹innerone›
2355+
(1) ‹innerone›
2356+
| ‹innertwo›
2357+
| outerthree
2358+
| outerfour
2359+
Wraps: (2) attached stack trace
2360+
-- stack trace:
2361+
| github.com/cockroachdb/errors/fmttests.glob...funcNN...
2362+
| <tab><path>:<lineno>
2363+
| github.com/cockroachdb/errors/fmttests.TestDatadriven.func2.1
2364+
| <tab><path>:<lineno>
2365+
| github.com/cockroachdb/datadriven.runDirective.func1
2366+
| <tab><path>:<lineno>
2367+
| github.com/cockroachdb/datadriven.runDirective
2368+
| <tab><path>:<lineno>
2369+
| github.com/cockroachdb/datadriven.runDirectiveOrSubTest
2370+
| <tab><path>:<lineno>
2371+
| github.com/cockroachdb/datadriven.runTestInternal
2372+
| <tab><path>:<lineno>
2373+
| github.com/cockroachdb/datadriven.RunTest
2374+
| <tab><path>:<lineno>
2375+
| github.com/cockroachdb/errors/fmttests.TestDatadriven.func2
2376+
| <tab><path>:<lineno>
2377+
| github.com/cockroachdb/datadriven.Walk
2378+
| <tab><path>:<lineno>
2379+
| github.com/cockroachdb/datadriven.Walk.func1
2380+
| <tab><path>:<lineno>
2381+
| testing.tRunner
2382+
| <tab><path>:<lineno>
2383+
| runtime.goexit
2384+
| <tab><path>:<lineno>
2385+
└─ Wraps: (3) outerthree
2386+
| outerfour
2387+
Wraps: (4) ‹innerone›
2388+
‹ | innertwo›
2389+
‹ | -- this is innerone›
2390+
‹ | innertwo's›
2391+
‹ | multi-line leaf payload›
2392+
Error types: (1) *join.joinError (2) *withstack.withStack (3) *errutil.leafError (4) *fmttests.errFmt
2393+
=====
2394+
===== Sentry reporting
2395+
=====
2396+
== Message payload
2397+
×
2398+
(1) ×
2399+
| ×
2400+
| outerthree
2401+
| outerfour
2402+
Wraps: (2) attached stack trace
2403+
-- stack trace:
2404+
| github.com/cockroachdb/errors/fmttests.glob...funcNN...
2405+
| <tab><path>:<lineno>
2406+
| github.com/cockroachdb/errors/fmttests.TestDatadriven.func2.1
2407+
| <tab><path>:<lineno>
2408+
| github.com/cockroachdb/datadriven.runDirective.func1
2409+
| <tab><path>:<lineno>
2410+
| github.com/cockroachdb/datadriven.runDirective
2411+
| <tab><path>:<lineno>
2412+
| github.com/cockroachdb/datadriven.runDirectiveOrSubTest
2413+
| <tab><path>:<lineno>
2414+
| github.com/cockroachdb/datadriven.runTestInternal
2415+
| <tab><path>:<lineno>
2416+
| github.com/cockroachdb/datadriven.RunTest
2417+
| <tab><path>:<lineno>
2418+
| github.com/cockroachdb/errors/fmttests.TestDatadriven.func2
2419+
| <tab><path>:<lineno>
2420+
| github.com/cockroachdb/datadriven.Walk
2421+
| <tab><path>:<lineno>
2422+
| github.com/cockroachdb/datadriven.Walk.func1
2423+
| <tab><path>:<lineno>
2424+
| testing.tRunner
2425+
| <tab><path>:<lineno>
2426+
| runtime.goexit
2427+
| <tab><path>:<lineno>
2428+
└─ Wraps: (3) outerthree
2429+
| outerfour
2430+
Wraps: (4) ×
2431+
×
2432+
×
2433+
×
2434+
×
2435+
Error types: (1) *join.joinError (2) *withstack.withStack (3) *errutil.leafError (4) *fmttests.errFmt
2436+
-- report composition:
2437+
*join.joinError
2438+
== Extra "error types"
2439+
github.com/cockroachdb/errors/join/*join.joinError (*::)
2440+
== Exception 1 (Module: "error domain: <none>")
2441+
Type: "*join.joinError"
2442+
Title: "×"
2443+
(NO STACKTRACE)
2444+
22542445
run
22552446
fmt innerone innertwo
22562447
migrated outerthree outerfour

0 commit comments

Comments
 (0)