Skip to content

Commit d7d2b24

Browse files
committed
Add Newf, Joinf, Messagef, remove WithWrapper, update docs
1 parent 2e5f512 commit d7d2b24

11 files changed

Lines changed: 585 additions & 224 deletions

README.md

Lines changed: 165 additions & 73 deletions
Large diffs are not rendered by default.

doc.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
// that simplify common error handling tasks by adding support for stack
44
// traces, combining multiple errors, and simplifying working with panics.
55
// The package maintains full compatibility with Go's standard error handling
6-
// features (Go 1.13+), including errors.As, errors.Is, and errors.Unwrap.
6+
// features (including changes in Go 1.13 and 1.20), such as errors.As,
7+
// errors.Is, and errors.Unwrap.
78
package xerrors

format.go

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ var errWriter io.Writer = os.Stderr
1313

1414
// Print writes a formatted error to stderr.
1515
//
16-
// If the error implements the [ErrorDetails] interface, the result
17-
// of [ErrorDetails] is used for each wrapped error. Otherwise, the
16+
// If the error implements the [DetailedError] interface, the result
17+
// of [DetailedError] is used for each wrapped error. Otherwise, the
1818
// standard Error method is used. The formatted error can span
1919
// multiple lines and always ends with a newline.
2020
func Print(err error) {
@@ -23,8 +23,8 @@ func Print(err error) {
2323

2424
// Sprint returns a formatted error as a string.
2525
//
26-
// If the error implements the [ErrorDetails] interface, the result
27-
// of [ErrorDetails] is used for each wrapped error. Otherwise, the
26+
// If the error implements the [DetailedError] interface, the result
27+
// of [DetailedError] is used for each wrapped error. Otherwise, the
2828
// standard Error method is used. The formatted error can span
2929
// multiple lines and always ends with a newline.
3030
func Sprint(err error) string {
@@ -35,8 +35,8 @@ func Sprint(err error) string {
3535

3636
// Fprint writes a formatted error to the provided [io.Writer].
3737
//
38-
// If the error implements the [ErrorDetails] interface, the result
39-
// of [ErrorDetails] is used for each wrapped error. Otherwise, the
38+
// If the error implements the [DetailedError] interface, the result
39+
// of [DetailedError] is used for each wrapped error. Otherwise, the
4040
// standard Error method is used. The formatted error can span
4141
// multiple lines and always ends with a newline.
4242
func Fprint(w io.Writer, err error) (int, error) {
@@ -45,36 +45,39 @@ func Fprint(w io.Writer, err error) (int, error) {
4545

4646
// fprint is a helper function that writes the formatted error to the
4747
// given [io.Writer].
48-
func fprint(w io.Writer, e error) (n int, err error) {
48+
//
49+
// This function will print all errors in the chain, that implement the
50+
// [DetailedError] interface, with the exception of the first error, which
51+
// will be printed using the standard Error method if it doesn't implement
52+
// the [DetailedError] interface.
53+
func fprint(w io.Writer, err error) (int, error) {
4954
const firstErrorPrefix = "Error: "
5055
const previousErrorPrefix = "Previous error: "
5156
var buffer bytes.Buffer
5257
first := true
53-
for e != nil {
54-
switch terr := e.(type) {
55-
case ErrorDetails:
58+
for err != nil {
59+
switch tErr := err.(type) {
60+
case DetailedError:
5661
if first {
5762
buffer.WriteString(firstErrorPrefix)
5863
} else {
5964
buffer.WriteString(previousErrorPrefix)
6065
}
61-
buffer.WriteString(terr.Error())
62-
buffer.WriteByte('\n')
63-
buffer.WriteString(terr.ErrorDetails())
66+
buffer.WriteString(tErr.DetailedError())
6467
default:
65-
// If an error does not implement the ErrorDetails interface,
66-
// then the Error() method will print all errors separated
68+
// If an error does not implement the DetailedError interface,
69+
// then the Error() method should print all errors separated
6770
// with ":", so there is no need to render each error other than
6871
// the first one.
6972
if first {
7073
buffer.WriteString(firstErrorPrefix)
71-
buffer.WriteString(terr.Error())
74+
buffer.WriteString(tErr.Error())
7275
buffer.WriteByte('\n')
7376
}
7477
}
7578
first = false
76-
if we, ok := e.(unwrapper); ok {
77-
e = we.Unwrap()
79+
if wErr, ok := err.(interface{ Unwrap() error }); ok {
80+
err = wErr.Unwrap()
7881
continue
7982
}
8083
break

format_test.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ func (e testErr) Error() string {
1616
return e.err
1717
}
1818

19-
func (e testErr) ErrorDetails() string {
20-
return e.details + "\n"
19+
func (e testErr) DetailedError() string {
20+
return e.err + "\n" + e.details + "\n"
2121
}
2222

2323
func (e testErr) Unwrap() error {
@@ -32,9 +32,6 @@ func TestFormat(t *testing.T) {
3232
{
3333
err: Message("foo"), want: "Error: foo\n",
3434
},
35-
{
36-
err: WithWrapper(Message("foo"), Message("bar")), want: "Error: foo: bar\n",
37-
},
3835
{
3936
err: testErr{err: "err", details: "details"},
4037
want: "Error: err\ndetails\n",
@@ -73,7 +70,7 @@ func TestPrint(t *testing.T) {
7370
}
7471

7572
func TestSprint(t *testing.T) {
76-
a := New("access denided")
73+
a := New("access denied")
7774
Print(a)
7875

7976
err := Message("foo")

multierror.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"strings"
77
)
88

9-
const multiErrorErrorPrefix = "the following errors occurred: "
9+
const multiErrorPrefix = "the following errors occurred:"
1010

1111
// Append appends the provided errors to an existing error or list of
1212
// errors. If `err` is not a [multiError], it will be converted into
@@ -19,6 +19,9 @@ const multiErrorErrorPrefix = "the following errors occurred: "
1919
// The returned error is compatible with Go errors, supporting
2020
// [errors.Is], [errors.As], and the Go 1.20 `Unwrap() []error`
2121
// method.
22+
//
23+
// To create a chained error, use [New], [Newf], [Join], or
24+
// [Joinf] instead.
2225
func Append(err error, errs ...error) error {
2326
var me multiError
2427
if err != nil {
@@ -54,8 +57,8 @@ type multiError []error
5457
// Error implements the [error] interface.
5558
func (e multiError) Error() string {
5659
var s strings.Builder
57-
s.WriteString(multiErrorErrorPrefix)
58-
s.WriteString("[")
60+
s.WriteString(multiErrorPrefix)
61+
s.WriteString(" [")
5962
for n, err := range e {
6063
s.WriteString(err.Error())
6164
if n < len(e)-1 {
@@ -66,9 +69,14 @@ func (e multiError) Error() string {
6669
return s.String()
6770
}
6871

69-
// ErrorDetails implements the [ErrorDetails] interface.
70-
func (e multiError) ErrorDetails() string {
72+
// DetailedError implements the [DetailedError] interface.
73+
func (e multiError) DetailedError() string {
74+
if len(e) == 0 {
75+
return ""
76+
}
7177
var s strings.Builder
78+
s.WriteString(multiErrorPrefix)
79+
s.WriteByte('\n')
7280
for n, err := range e.Unwrap() {
7381
s.WriteString(strconv.Itoa(n + 1))
7482
s.WriteString(". ")

multierror_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,22 +60,22 @@ func TestAppend(t *testing.T) {
6060
}
6161
}
6262

63-
func TestMultiError_ErrorDetails(t *testing.T) {
63+
func TestMultiError_DetailedError(t *testing.T) {
6464
tests := []struct {
6565
errs []error
6666
want string
6767
regexp bool
6868
}{
6969
{errs: []error{}, want: ``},
70-
{errs: []error{Message("a")}, want: "1. Error: a\n"},
71-
{errs: []error{Message("a"), Message("b")}, want: "1. Error: a\n2. Error: b\n"},
72-
{errs: []error{Message("a"), multiError{Message("b"), Message("c")}}, want: "1. Error: a\n2. Error: the following errors occurred: [b, c]\n\t1. Error: b\n\t2. Error: c\n"},
70+
{errs: []error{Message("a")}, want: "the following errors occurred:\n1. Error: a\n"},
71+
{errs: []error{Message("a"), Message("b")}, want: "the following errors occurred:\n1. Error: a\n2. Error: b\n"},
72+
{errs: []error{Message("a"), multiError{Message("b"), Message("c")}}, want: "the following errors occurred:\n1. Error: a\n2. Error: the following errors occurred:\n\t1. Error: b\n\t2. Error: c\n"},
7373
}
7474
for n, tt := range tests {
7575
t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) {
7676
err := multiError(tt.errs)
77-
if got := err.ErrorDetails(); got != tt.want {
78-
t.Errorf("multiError(errs).ErrorDetails(): %q does not match %q", got, tt.want)
77+
if got := err.DetailedError(); got != tt.want {
78+
t.Errorf("multiError(errs).DetailedError(): %q does not match %q", got, tt.want)
7979
}
8080
})
8181
}

stacktrace.go

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,22 @@ import (
1010

1111
const stackTraceDepth = 32
1212

13-
// StackTrace extracts the stack trace from the given error or the
14-
// first wrapped error that implements [stackTracer].
13+
// StackTrace extracts the stack trace from the provided error.
14+
// It traverses the error chain, looking for the last error that
15+
// has a stack trace.
1516
func StackTrace(err error) Callers {
17+
var callers Callers
1618
for err != nil {
17-
if e, ok := err.(stackTracer); ok {
18-
return e.StackTrace()
19+
if e, ok := err.(interface{ StackTrace() Callers }); ok {
20+
callers = e.StackTrace()
1921
}
20-
if e, ok := err.(unwrapper); ok {
22+
if e, ok := err.(interface{ Unwrap() error }); ok {
2123
err = e.Unwrap()
2224
continue
2325
}
2426
break
2527
}
26-
return nil
28+
return callers
2729
}
2830

2931
// WithStackTrace wraps the provided error with a stack trace,
@@ -52,9 +54,13 @@ func (e *withStackTrace) Error() string {
5254
return e.err.Error()
5355
}
5456

55-
// ErrorDetails implements the [ErrorDetails] interface.
56-
func (e *withStackTrace) ErrorDetails() string {
57-
return e.stack.String()
57+
// DetailedError implements the [DetailedError] interface.
58+
func (e *withStackTrace) DetailedError() string {
59+
s := &strings.Builder{}
60+
s.WriteString(e.err.Error())
61+
s.WriteString("\n")
62+
s.WriteString(e.stack.String())
63+
return s.String()
5864
}
5965

6066
// Unwrap implements the Go 1.13 `Unwrap() error` method, returning
@@ -200,12 +206,6 @@ func (c Callers) writeTrace(w io.Writer) {
200206
}
201207
}
202208

203-
// stackTracer represents an error that provides a stack trace.
204-
type stackTracer interface {
205-
error
206-
StackTrace() Callers
207-
}
208-
209209
// callers captures the current stack trace, skipping the specified
210210
// number of frames.
211211
func callers(skip int) Callers {

wrapper.go

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,45 +5,36 @@ import (
55
"strings"
66
)
77

8-
// WithWrapper wraps `err` with a `wrapper` error.
9-
//
10-
// The `wrapper` should generally be a simple, sentinel error, as
11-
// details like its stack trace are ignored. The `Unwrap` method
12-
// will only unwrap `err`, but [errors.Is] and [errors.As] work
13-
// with both `wrapper` and `err`.
14-
//
15-
// If `wrapper` is nil, `err` is returned. If `err` is nil,
16-
// WithWrapper returns nil.
17-
func WithWrapper(wrapper error, err error) error {
18-
if err == nil {
19-
return nil
20-
}
21-
if wrapper == nil {
22-
return err
23-
}
24-
return &withWrapper{
25-
wrapper: wrapper,
26-
err: err,
27-
}
28-
}
29-
308
// withWrapper wraps an error with another error.
9+
//
10+
// It is intended to be build error chains, e.g. if we have a
11+
// following error chain: `err1: err2: err3`, the wrapper is `err1`,
12+
// and the err is another withWrapper containing `err2` and `err3`.
3113
type withWrapper struct {
32-
wrapper error
33-
err error
14+
wrapper error // wrapper is the error that wraps the next error in the chain, may be nil
15+
err error // err is the next error in the chain, must not be nil
16+
msg string // msg overwrites the error message, if set
3417
}
3518

3619
// Error implements the [error] interface.
3720
func (e *withWrapper) Error() string {
21+
if e.msg != "" {
22+
return e.msg
23+
}
3824
s := &strings.Builder{}
39-
s.WriteString(e.wrapper.Error())
40-
s.WriteString(": ")
25+
if e.wrapper != nil {
26+
s.WriteString(e.wrapper.Error())
27+
s.WriteString(": ")
28+
}
4129
s.WriteString(e.err.Error())
4230
return s.String()
4331
}
4432

4533
// Unwrap implements the Go 1.13 `Unwrap() error` method, returning
4634
// the wrapped error.
35+
//
36+
// Since withWrapper represents a chain of errors, the Unwrap method
37+
// returns the next error in the chain, not both the wrapper and the error.
4738
func (e *withWrapper) Unwrap() error {
4839
return e.err
4940
}

wrapper_test.go

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,42 @@ import (
88
"testing"
99
)
1010

11-
func TestWrap(t *testing.T) {
11+
func TestWithWrapper(t *testing.T) {
1212
tests := []struct {
13-
err error
1413
wrapper error
14+
err error
15+
msg string
1516
want string
16-
wantNil bool
1717
}{
18-
{err: Message("err"), wrapper: Message("wrapper"), want: "wrapper: err"},
19-
{err: io.EOF, wrapper: Message("wrapper"), want: "wrapper: EOF"},
20-
{err: nil, wrapper: Message("wrapper"), wantNil: true},
21-
{err: Message("err"), wrapper: nil, want: "err"},
18+
{wrapper: Message("wrapper"), err: Message("err"), want: "wrapper: err"},
19+
{wrapper: Message("wrapper"), err: io.EOF, want: "wrapper: EOF"},
20+
{wrapper: nil, err: Message("err"), want: "err"},
21+
{wrapper: Message("wrapper"), err: Message("err"), msg: "msg", want: "msg"},
2222
}
2323
for n, tt := range tests {
2424
t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) {
25-
got := WithWrapper(tt.wrapper, tt.err)
26-
switch {
27-
case tt.wantNil:
28-
if got != nil {
29-
t.Errorf("WithWrapper(%#v, %#v): expected nil", tt.wrapper, tt.err)
30-
}
31-
default:
32-
if got.Error() != tt.want {
33-
t.Errorf("WithWrapper(%#v, %#v): got: %q, want %q", tt.wrapper, tt.err, got, tt.want)
34-
}
35-
if len(StackTrace(got)) != 0 {
36-
t.Errorf("WithWrapper(%#v, %#v): returned error must not contain a stack trace", tt.wrapper, tt.err)
37-
}
38-
if !errors.Is(got, tt.err) {
39-
t.Errorf("WithWrapper(%#v, %#v): errors.Is must return true for err", tt.wrapper, tt.err)
40-
}
41-
if tt.wrapper != nil && !errors.Is(got, tt.wrapper) {
42-
t.Errorf("WithWrapper(%#v, %#v): errors.Is must return true for wrapper", tt.wrapper, tt.err)
43-
}
44-
if tt.err != nil && !errors.As(got, reflect.New(reflect.TypeOf(tt.err)).Interface()) {
45-
t.Errorf("errors.As(WithWrapper(%#v, %#v), err): must return true for the err error type", tt.wrapper, tt.err)
46-
}
47-
if tt.wrapper != nil && !errors.As(got, reflect.New(reflect.TypeOf(tt.wrapper)).Interface()) {
48-
t.Errorf("errors.As(WithWrapper(%#v, %#v), err): must return true for the wrapper error type", tt.wrapper, tt.err)
49-
}
25+
got := &withWrapper{
26+
wrapper: tt.wrapper,
27+
err: tt.err,
28+
msg: tt.msg,
29+
}
30+
if got.Error() != tt.want {
31+
t.Errorf("WithWrapper(%#v, %#v): got: %q, want %q", tt.wrapper, tt.err, got, tt.want)
32+
}
33+
if len(StackTrace(got)) != 0 {
34+
t.Errorf("WithWrapper(%#v, %#v): returned error must not contain a stack trace", tt.wrapper, tt.err)
35+
}
36+
if !errors.Is(got, tt.err) {
37+
t.Errorf("WithWrapper(%#v, %#v): errors.Is must return true for err", tt.wrapper, tt.err)
38+
}
39+
if tt.wrapper != nil && !errors.Is(got, tt.wrapper) {
40+
t.Errorf("WithWrapper(%#v, %#v): errors.Is must return true for wrapper", tt.wrapper, tt.err)
41+
}
42+
if tt.err != nil && !errors.As(got, reflect.New(reflect.TypeOf(tt.err)).Interface()) {
43+
t.Errorf("errors.As(WithWrapper(%#v, %#v), err): must return true for the err error type", tt.wrapper, tt.err)
44+
}
45+
if tt.wrapper != nil && !errors.As(got, reflect.New(reflect.TypeOf(tt.wrapper)).Interface()) {
46+
t.Errorf("errors.As(WithWrapper(%#v, %#v), err): must return true for the wrapper error type", tt.wrapper, tt.err)
5047
}
5148
})
5249
}

0 commit comments

Comments
 (0)