├── go.mod ├── .golangci.yml ├── doc.go ├── .github └── workflows │ ├── coverage.yaml │ └── go.yaml ├── LICENSE ├── wrapper_test.go ├── panic.go ├── wrapper.go ├── xerrors_go120_test.go ├── format_test.go ├── multierror.go ├── multierror_test.go ├── format.go ├── panic_test.go ├── stacktrace_test.go ├── stacktrace.go ├── xerrors.go ├── xerrors_test.go └── README.md /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mdobak/go-xerrors 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | disable: 4 | - errcheck 5 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package xerrors is a small, idiomatic library that makes error handling in 2 | // Go easier. It provides utilities for creating errors with stack traces, 3 | // wrapping existing errors, aggregating multiple errors, and recovering 4 | // from panics. 5 | package xerrors 6 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yaml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - main 8 | pull_request: 9 | 10 | jobs: 11 | coverage: 12 | name: Report Coverage 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Install Go 16 | uses: actions/setup-go@v4 17 | with: 18 | go-version: "1.24.x" 19 | 20 | - name: Check out code 21 | uses: actions/checkout@v4 22 | 23 | - name: Install deps 24 | run: | 25 | go mod download 26 | 27 | - name: Run tests with coverage output 28 | run: | 29 | go test -race -covermode atomic -coverprofile=covprofile ./... 30 | 31 | - name: Install goveralls 32 | run: go install github.com/mattn/goveralls@latest 33 | 34 | - name: Send coverage 35 | env: 36 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | run: goveralls -coverprofile=covprofile -service=github 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Michał Dobaczewski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/go.yaml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | tags: [ 'v*' ] 7 | pull_request: 8 | 9 | jobs: 10 | CI: 11 | strategy: 12 | matrix: 13 | go_version: [ "1.18.x", "1.24.x" ] 14 | 15 | runs-on: "ubuntu-latest" 16 | steps: 17 | - uses: "actions/checkout@v4" 18 | 19 | - name: "Set up Go" 20 | uses: "actions/setup-go@v5" 21 | with: 22 | go-version: ${{ matrix.go_version }} 23 | 24 | - name: "Build" 25 | run: "go build -v ./..." 26 | 27 | - name: "Test" 28 | run: "go test -v ./..." 29 | 30 | - name: "Linter" 31 | uses: "golangci/golangci-lint-action@v7" 32 | with: 33 | version: "v2.0" 34 | 35 | Release: 36 | if: startsWith(github.ref, 'refs/tags/') 37 | needs: CI 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Create Release 41 | id: create_release 42 | uses: actions/create-release@v1 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | with: 46 | tag_name: ${{ github.ref }} 47 | release_name: Release ${{ github.ref }} 48 | draft: false 49 | prerelease: false 50 | -------------------------------------------------------------------------------- /wrapper_test.go: -------------------------------------------------------------------------------- 1 | package xerrors 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestWithWrapper(t *testing.T) { 12 | tests := []struct { 13 | wrapper error 14 | err error 15 | msg string 16 | want string 17 | }{ 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"}, 22 | } 23 | for n, tt := range tests { 24 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 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) 47 | } 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /panic.go: -------------------------------------------------------------------------------- 1 | package xerrors 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // PanicError represents an error that occurs during a panic. It is 8 | // returned by the [Recover] and [FromRecover] functions. It provides 9 | // access to the original panic value via the [Panic] method. 10 | // 11 | // Use the standard [errors.As] function to convert an error to this interface. 12 | type PanicError interface { 13 | error 14 | 15 | // Panic returns the raw panic value. 16 | Panic() any 17 | } 18 | 19 | // Recover wraps the built-in `recover()` function, converting the 20 | // recovered value into an error with a stack trace. The provided `fn` 21 | // callback is only invoked when a panic occurs. 22 | // 23 | // This function must always be used directly with the `defer` 24 | // keyword. 25 | func Recover(fn func(err error)) { 26 | if r := recover(); r != nil { 27 | fn(&withStackTrace{ 28 | err: &panicError{panic: r}, 29 | stack: callers(2), 30 | }) 31 | } 32 | } 33 | 34 | // FromRecover converts the result of the built-in `recover()` into 35 | // an error with a stack trace. The returned error implements 36 | // [PanicError]. Returns nil if `r` is nil. 37 | // 38 | // This function must be called in the same function as `recover()` 39 | // to ensure the stack trace is accurate. 40 | func FromRecover(r any) error { 41 | if r == nil { 42 | return nil 43 | } 44 | return &withStackTrace{ 45 | err: &panicError{panic: r}, 46 | stack: callers(3), 47 | } 48 | } 49 | 50 | // panicError represents an error that occurs during a panic, 51 | // constructed from the value returned by `recover()`. 52 | type panicError struct { 53 | panic any 54 | } 55 | 56 | // Panic implements the [PanicError] interface. 57 | func (e *panicError) Panic() any { 58 | return e.panic 59 | } 60 | 61 | // Error implements the [error] interface. 62 | func (e *panicError) Error() string { 63 | return fmt.Sprintf("panic: %v", e.panic) 64 | } 65 | -------------------------------------------------------------------------------- /wrapper.go: -------------------------------------------------------------------------------- 1 | package xerrors 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | // withWrapper wraps one error with another. 9 | // 10 | // It is intended to build error chains. For example, given an error chain 11 | // like `err1: err2: err3`, the wrapper is `err1`, and the err is another 12 | // withWrapper containing `err2` and `err3`. 13 | type withWrapper struct { 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 17 | } 18 | 19 | // Error implements the [error] interface. 20 | func (e *withWrapper) Error() string { 21 | if e.msg != "" { 22 | return e.msg 23 | } 24 | s := &strings.Builder{} 25 | if e.wrapper != nil { 26 | s.WriteString(e.wrapper.Error()) 27 | s.WriteString(": ") 28 | } 29 | s.WriteString(e.err.Error()) 30 | return s.String() 31 | } 32 | 33 | // ErrorDetails implements the [DetailedError] interface. 34 | func (e *withWrapper) ErrorDetails() string { 35 | err := e.wrapper 36 | for err != nil { 37 | if dErr, ok := err.(DetailedError); ok { 38 | return dErr.ErrorDetails() 39 | } 40 | if wErr, ok := err.(interface{ Unwrap() error }); ok { 41 | err = wErr.Unwrap() 42 | continue 43 | } 44 | break 45 | } 46 | return "" 47 | } 48 | 49 | // Unwrap implements the Go 1.13 `Unwrap() error` method, returning 50 | // the wrapped error. 51 | // 52 | // Since withWrapper represents a chain of errors, the Unwrap method 53 | // returns the next error in the chain, not both the wrapper and the error. 54 | func (e *withWrapper) Unwrap() error { 55 | return e.err 56 | } 57 | 58 | // As implements the Go 1.13 `errors.As` method, allowing type 59 | // assertions on all errors in the list. 60 | func (e *withWrapper) As(target any) bool { 61 | return errors.As(e.wrapper, target) || errors.As(e.err, target) 62 | } 63 | 64 | // Is implements the Go 1.13 `errors.Is` method, allowing 65 | // comparisons with all errors in the list. 66 | func (e *withWrapper) Is(target error) bool { 67 | return errors.Is(e.wrapper, target) || errors.Is(e.err, target) 68 | } 69 | -------------------------------------------------------------------------------- /xerrors_go120_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.20 2 | // +build go1.20 3 | 4 | package xerrors 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "testing" 10 | ) 11 | 12 | func TestJoinf_Go120(t *testing.T) { 13 | err1 := Message("first error") 14 | err2 := Message("second error") 15 | tests := []struct { 16 | format string 17 | args []any 18 | want string 19 | }{ 20 | {format: "multiple errors: %w: %w", args: []any{err1, err2}, want: "multiple errors: first error: second error"}, 21 | {format: "wrapped multiple nil errors: %w %w", args: []any{nil, nil}, want: "wrapped multiple nil errors: %!w() %!w()"}, 22 | {format: "first error nil: %w %w", args: []any{nil, err2}, want: "first error nil: %!w() second error"}, 23 | {format: "second error nil: %w %w", args: []any{err1, nil}, want: "second error nil: first error %!w()"}, 24 | } 25 | for n, tt := range tests { 26 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 27 | got := Joinf(tt.format, tt.args...) 28 | if got == nil { 29 | t.Errorf("Joinf(%q, %#v): expected non-nil error", tt.format, tt.args) 30 | return 31 | } 32 | if got.Error() != tt.want { 33 | t.Errorf("Joinf(%q, %#v): got: %q, want %q", tt.format, tt.args, got, tt.want) 34 | } 35 | if len(StackTrace(got)) != 0 { 36 | t.Errorf("Joinf(%q, %#v): returned error must not contain a stack trace", tt.format, tt.args) 37 | } 38 | for _, v := range tt.args { 39 | if err, ok := v.(error); ok { 40 | if !errors.Is(got, err) { 41 | t.Errorf("errors.Is(Joinf(errs...), err): must return true") 42 | } 43 | } 44 | } 45 | }) 46 | } 47 | } 48 | 49 | func TestJoinf_Unwrap_Go120(t *testing.T) { 50 | err1 := Message("first error") 51 | err2 := Message("second error") 52 | got := Joinf("%w: %w", err1, err2) 53 | unwrapper, ok := got.(interface{ Unwrap() error }) 54 | if !ok { 55 | t.Fatalf("Join(err1, err2) must implement Unwrap()") 56 | } 57 | unwrapped := unwrapper.Unwrap() 58 | if unwrapped == nil { 59 | t.Fatalf("Join(err1, err2).Unwrap() must not return nil") 60 | } 61 | if errors.Is(unwrapped, err1) || !errors.Is(unwrapped, err2) { 62 | t.Fatalf("Join(err1, err2).Unwrap() must return the second error") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /format_test.go: -------------------------------------------------------------------------------- 1 | package xerrors 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | type testErr struct { 10 | err string 11 | details string 12 | wrapped error 13 | } 14 | 15 | func (e testErr) Error() string { 16 | return e.err 17 | } 18 | 19 | func (e testErr) ErrorDetails() string { 20 | return e.details + "\n" 21 | } 22 | 23 | func (e testErr) Unwrap() error { 24 | return e.wrapped 25 | } 26 | 27 | func TestFormat(t *testing.T) { 28 | tests := []struct { 29 | err error 30 | want string 31 | }{ 32 | { 33 | err: Message("foo"), want: "Error: foo\n", 34 | }, 35 | { 36 | err: testErr{err: "err", details: "details"}, 37 | want: "Error: err\n\tdetails\n", 38 | }, 39 | { 40 | err: testErr{err: "err", details: "details", wrapped: Message("wrapped")}, 41 | want: "Error: err\n\tdetails\n", 42 | }, 43 | { 44 | err: testErr{err: "err", details: "details", wrapped: testErr{err: "wrapped err", details: "wrapped details"}}, 45 | want: "Error: err\n\tdetails\nPrevious error: wrapped err\n\twrapped details\n", 46 | }, 47 | } 48 | for n, tt := range tests { 49 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 50 | if got := Sprint(tt.err); got != tt.want { 51 | t.Errorf("Sprint(%#v): %q does not match %q", tt.err, got, tt.want) 52 | } 53 | }) 54 | } 55 | } 56 | 57 | func TestPrint(t *testing.T) { 58 | prevErrWriter := errWriter 59 | defer func() { errWriter = prevErrWriter }() 60 | 61 | err := Message("foo") 62 | buf := &strings.Builder{} 63 | errWriter = buf 64 | Print(err) 65 | got := buf.String() 66 | exp := "Error: foo\n" 67 | if got != exp { 68 | t.Errorf("Print(buf, %#v): wrote invalid error message, got %q but %q expected", err, got, exp) 69 | } 70 | } 71 | 72 | func TestSprint(t *testing.T) { 73 | a := New("access denied") 74 | Print(a) 75 | 76 | err := Message("foo") 77 | got := Sprint(err) 78 | exp := "Error: foo\n" 79 | if got != exp { 80 | t.Errorf("Sprint(b, %#v): wrote invalid error message, got %q but %q expected", err, got, exp) 81 | } 82 | } 83 | 84 | func TestFprint(t *testing.T) { 85 | err := Message("foo") 86 | buf := &strings.Builder{} 87 | n, werr := Fprint(buf, err) 88 | got := buf.String() 89 | exp := "Error: foo\n" 90 | if werr != nil { 91 | t.Errorf("Fprint(buf, %#v): returned an error", err) 92 | } 93 | if n != 11 { 94 | t.Errorf("Fprint(buf, %#v): returned invalid number of bytes", err) 95 | } 96 | if got != exp { 97 | t.Errorf("Fprint(buf, %#v): wrote invalid error message, got %q but %q expected", err, got, exp) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /multierror.go: -------------------------------------------------------------------------------- 1 | package xerrors 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // Append appends the provided errors to an existing error or list of 10 | // errors. If `err` is not a [multiError], it will be converted into 11 | // one. Nil errors are ignored. It does not record a stack trace. 12 | // 13 | // If the resulting error list is empty, nil is returned. If the 14 | // resulting error list contains only one error, that error is 15 | // returned instead of the list. 16 | // 17 | // The returned error is compatible with Go errors, supporting 18 | // [errors.Is], [errors.As], and the Go 1.20 `Unwrap() []error` 19 | // method. 20 | // 21 | // To create a chained error, use [New], [Newf], [Join], or 22 | // [Joinf] instead. 23 | func Append(err error, errs ...error) error { 24 | var me multiError 25 | if err != nil { 26 | if mErr, ok := err.(multiError); ok { 27 | me = mErr 28 | } else { 29 | me = multiError{err} 30 | } 31 | } 32 | for _, e := range errs { 33 | if e != nil { 34 | me = append(me, e) 35 | } 36 | } 37 | switch len(me) { 38 | case 0: 39 | return nil 40 | case 1: 41 | return me[0] 42 | default: 43 | return me 44 | } 45 | } 46 | 47 | // multiError is a slice of errors that can be treated as a single 48 | // error. 49 | type multiError []error 50 | 51 | // Error implements the [error] interface. 52 | func (e multiError) Error() string { 53 | var s strings.Builder 54 | s.WriteString("[") 55 | for n, err := range e { 56 | s.WriteString(err.Error()) 57 | if n < len(e)-1 { 58 | s.WriteString(", ") 59 | } 60 | } 61 | s.WriteString("]") 62 | return s.String() 63 | } 64 | 65 | // ErrorDetails returns additional details about the error for 66 | // the [ErrorDetails] function. 67 | func (e multiError) ErrorDetails() string { 68 | if len(e) == 0 { 69 | return "" 70 | } 71 | buf := &strings.Builder{} 72 | for n, err := range e.Unwrap() { 73 | buf.WriteString(strconv.Itoa(n + 1)) 74 | buf.WriteString(". ") 75 | writeErr(buf, err) 76 | } 77 | return buf.String() 78 | } 79 | 80 | // Unwrap implements the Go 1.20 `Unwrap() []error` method, returning 81 | // a slice containing all errors in the list. 82 | func (e multiError) Unwrap() []error { 83 | s := make([]error, len(e)) 84 | copy(s, e) 85 | return s 86 | } 87 | 88 | // As implements the Go 1.13 `errors.As` method, allowing type 89 | // assertions on all errors in the list. 90 | func (e multiError) As(target any) bool { 91 | for _, err := range e { 92 | if errors.As(err, target) { 93 | return true 94 | } 95 | } 96 | return false 97 | } 98 | 99 | // Is implements the Go 1.13 `errors.Is` method, allowing 100 | // comparisons with all errors in the list. 101 | func (e multiError) Is(target error) bool { 102 | for _, err := range e { 103 | if errors.Is(err, target) { 104 | return true 105 | } 106 | } 107 | return false 108 | } 109 | -------------------------------------------------------------------------------- /multierror_test.go: -------------------------------------------------------------------------------- 1 | package xerrors 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestAppend(t *testing.T) { 11 | tests := []struct { 12 | err error 13 | errs []error 14 | want string 15 | wantNil bool 16 | }{ 17 | {err: nil, errs: []error{Message("a"), Message("b")}, want: "[a, b]"}, 18 | {err: nil, errs: []error{nil, Message("a")}, want: "a"}, 19 | {err: Message("a"), errs: []error{Message("b"), Message("c")}, want: "[a, b, c]"}, 20 | {err: Message("a"), errs: nil, want: "a"}, 21 | {err: multiError{Message("a"), Message("b")}, errs: nil, want: "[a, b]"}, 22 | {err: multiError{Message("a"), Message("b")}, errs: []error{Message("c")}, want: "[a, b, c]"}, 23 | {err: multiError{}, errs: []error{Message("a"), nil}, want: "a"}, 24 | {err: nil, errs: nil, wantNil: true}, 25 | {err: nil, errs: []error{nil, nil}, wantNil: true}, 26 | } 27 | for n, tt := range tests { 28 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 29 | got := Append(tt.err, tt.errs...) 30 | if tt.wantNil { 31 | if got != nil { 32 | t.Errorf("Append(nil): must return nil") 33 | } 34 | } else { 35 | if got.Error() != tt.want { 36 | t.Errorf("Append(err, errs...).Error(): got: %q, want %q", got, tt.want) 37 | } 38 | if len(StackTrace(got)) != 0 { 39 | t.Errorf("Append(err, errs...): returned error must not contain a stack trace") 40 | } 41 | if errors.Is(got, Message("foo")) { 42 | t.Errorf("errors.Is(Append(err, errs...), err): must return false for not included error") 43 | } 44 | if errors.As(got, reflect.New(reflect.TypeOf(&withWrapper{})).Interface()) { 45 | t.Errorf("errors.As(Append(err, errs...), err): must return false for a different error type") 46 | } 47 | for _, err := range tt.errs { 48 | if err == nil { 49 | continue 50 | } 51 | if !errors.Is(got, err) { 52 | t.Errorf("errors.Is(Append(err, errs...), errs[n]): must return true for all errors") 53 | } 54 | if !errors.As(got, reflect.New(reflect.TypeOf(err)).Interface()) { 55 | t.Errorf("errors.As(Append(err, errs...), errs[n]): must return true for all errors") 56 | } 57 | } 58 | } 59 | }) 60 | } 61 | } 62 | 63 | func TestMultiError_ErrorDetails(t *testing.T) { 64 | tests := []struct { 65 | errs []error 66 | want string 67 | regexp bool 68 | }{ 69 | {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: [b, c]\n\t1. Error: b\n\t2. Error: c\n"}, 73 | } 74 | for n, tt := range tests { 75 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 76 | 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) 79 | } 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /format.go: -------------------------------------------------------------------------------- 1 | package xerrors 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | var errWriter io.Writer = os.Stderr 12 | 13 | // Print writes a formatted error to stderr. 14 | // 15 | // If any error in the chain implements [DetailedError] and returns a non-empty 16 | // string, its details are appended to the error message. 17 | // 18 | // The output may span multiple lines and always ends with a newline. 19 | func Print(err error) { 20 | buf := &strings.Builder{} 21 | writeErr(buf, err) 22 | errWriter.Write([]byte(buf.String())) 23 | } 24 | 25 | // Sprint returns a formatted error as a string. 26 | // 27 | // If any error in the chain implements [DetailedError] and returns a non-empty 28 | // string, its details are appended to the error message. 29 | // 30 | // The output may span multiple lines and always ends with a newline. 31 | func Sprint(err error) string { 32 | buf := &strings.Builder{} 33 | writeErr(buf, err) 34 | return buf.String() 35 | } 36 | 37 | // Fprint writes a formatted error to the provided [io.Writer]. 38 | // 39 | // If any error in the chain implements [DetailedError] and returns a non-empty 40 | // string, its details are appended to the error message. 41 | // 42 | // The output may span multiple lines and always ends with a newline. 43 | func Fprint(w io.Writer, err error) (int, error) { 44 | buf := &strings.Builder{} 45 | writeErr(buf, err) 46 | return w.Write([]byte(buf.String())) 47 | } 48 | 49 | // writeErr writes a formatted error to the provided strings.Builder. 50 | func writeErr(buf *strings.Builder, err error) { 51 | const firstErrorPrefix = "Error: " 52 | const previousErrorPrefix = "Previous error: " 53 | first := true 54 | for err != nil { 55 | errMsg := err.Error() 56 | errDetails := "" 57 | if dErr, ok := err.(DetailedError); ok { 58 | errDetails = dErr.ErrorDetails() 59 | } 60 | if errDetails != "" { 61 | if first { 62 | buf.WriteString(firstErrorPrefix) 63 | } else { 64 | buf.WriteString(previousErrorPrefix) 65 | } 66 | buf.WriteString(errMsg) 67 | buf.WriteString("\n\t") 68 | buf.WriteString(indent(errDetails)) 69 | if !strings.HasSuffix(errDetails, "\n") { 70 | buf.WriteByte('\n') 71 | } 72 | } else { 73 | // If an error does not contain any details, do not print 74 | // it, except for the first one. This is to avoid printing 75 | // every wrapped error in the chain. 76 | if first { 77 | buf.WriteString(firstErrorPrefix) 78 | buf.WriteString(errMsg) 79 | buf.WriteByte('\n') 80 | } 81 | } 82 | first = false 83 | if wErr, ok := err.(interface{ Unwrap() error }); ok { 84 | err = wErr.Unwrap() 85 | continue 86 | } 87 | break 88 | } 89 | } 90 | 91 | // format is a helper function that formats a value according to the provided 92 | // format state and verb. 93 | func format(s fmt.State, verb rune, v any) { 94 | f := []rune{'%'} 95 | for _, c := range []int{'-', '+', '#', ' ', '0'} { 96 | if s.Flag(c) { 97 | f = append(f, rune(c)) 98 | } 99 | } 100 | if w, ok := s.Width(); ok { 101 | f = append(f, []rune(strconv.Itoa(w))...) 102 | } 103 | if p, ok := s.Precision(); ok { 104 | f = append(f, '.') 105 | f = append(f, []rune(strconv.Itoa(p))...) 106 | } 107 | f = append(f, verb) 108 | fmt.Fprintf(s, string(f), v) 109 | } 110 | 111 | // indent indents every line, except the first one, with a tab. 112 | func indent(s string) string { 113 | nl := strings.HasSuffix(s, "\n") 114 | if nl { 115 | s = s[:len(s)-1] 116 | } 117 | s = strings.ReplaceAll(s, "\n", "\n\t") 118 | if nl { 119 | s += "\n" 120 | } 121 | return s 122 | } 123 | -------------------------------------------------------------------------------- /panic_test.go: -------------------------------------------------------------------------------- 1 | package xerrors 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "testing" 8 | ) 9 | 10 | func TestRecover(t *testing.T) { 11 | tests := []struct { 12 | panic any 13 | want string 14 | }{ 15 | {panic: nil, want: ""}, 16 | {panic: "foo", want: "panic: foo"}, 17 | {panic: 42, want: "panic: 42"}, 18 | } 19 | for n, tt := range tests { 20 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 21 | handled := false 22 | defer func() { 23 | if tt.panic != nil && !handled { 24 | t.Errorf("Recover(): callback was not called during panicking") 25 | } 26 | if tt.panic == nil && handled { 27 | t.Errorf("Recover(): callback was called without panickng") 28 | } 29 | }() 30 | defer Recover(func(got error) { 31 | handled = true 32 | if got.Error() != tt.want { 33 | t.Errorf("Recover(): got: %q, want %q", got, tt.want) 34 | } 35 | st := StackTrace(got) 36 | if len(st) == 0 { 37 | t.Errorf("Recover(): created error must contain a stack trace") 38 | } 39 | if len(st) > 0 && shortname(st.Frames()[0].Function) != "go-xerrors.TestRecover.func1" { 40 | t.Errorf("Recover(): the first frame of stack trace must start at xerrors.TestRecover.func1") 41 | } 42 | panicErr := &panicError{} 43 | if errors.As(got, &panicErr); panicErr.Panic() != tt.panic { 44 | t.Errorf("Recover(): the value returned by Panic method must be the same as the value used to invoke panic") 45 | } 46 | }) 47 | if tt.panic != nil { 48 | panic(tt.panic) 49 | } 50 | }) 51 | } 52 | } 53 | 54 | func TestFromRecover(t *testing.T) { 55 | tests := []struct { 56 | panic any 57 | want string 58 | }{ 59 | {panic: nil, want: ""}, 60 | {panic: "foo", want: "panic: foo"}, 61 | {panic: 42, want: "panic: 42"}, 62 | } 63 | for n, tt := range tests { 64 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 65 | defer func() { 66 | got := FromRecover(recover()) 67 | if tt.panic == nil { 68 | if got != nil { 69 | t.Errorf("FromRecover(nil): got: %q, want %q", got, tt.want) 70 | } 71 | } else { 72 | if got.Error() != tt.want { 73 | t.Errorf("FromRecover(): got: %q, want %q", got, tt.want) 74 | } 75 | st := StackTrace(got) 76 | if len(st) == 0 { 77 | t.Errorf("FromRecover(): created error must contain a stack trace") 78 | } 79 | if len(st) > 0 && shortname(st.Frames()[0].Function) != "go-xerrors.TestFromRecover.func1" { 80 | t.Errorf("FromRecover(): the first frame of stack trace must start at xerrors.TestFromRecover.func1") 81 | } 82 | panicErr := &panicError{} 83 | if errors.As(got, &panicErr); panicErr.Panic() != tt.panic { 84 | t.Errorf("FromRecover(): the value returned by Panic method must be the same as the value used to invoke panic") 85 | } 86 | } 87 | }() 88 | panic(tt.panic) 89 | }) 90 | } 91 | } 92 | 93 | func TestPanicErrorFormat(t *testing.T) { 94 | tests := []struct { 95 | format string 96 | want string 97 | regexp bool 98 | }{ 99 | {format: "%s", want: `panic: foo`}, 100 | {format: "%v", want: `panic: foo`}, 101 | {format: "%q", want: `"panic: foo"`}, 102 | {format: "%+q", want: `"panic: foo"`}, 103 | } 104 | for n, tt := range tests { 105 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 106 | defer Recover(func(err error) { 107 | if tt.regexp { 108 | got := fmt.Sprintf(tt.format, err) 109 | if match, _ := regexp.MatchString(tt.want, got); !match { 110 | t.Errorf("fmt.Sprtinf(%q, &panicError{...}): %q does not match %q", tt.format, got, tt.want) 111 | } 112 | } else { 113 | if got := fmt.Sprintf(tt.format, err); got != tt.want { 114 | t.Errorf("fmt.Sprtinf(%q, &panicError{...}): got: %q, want: %q", tt.format, got, tt.want) 115 | } 116 | } 117 | }) 118 | panic("foo") 119 | }) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /stacktrace_test.go: -------------------------------------------------------------------------------- 1 | package xerrors 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "reflect" 8 | "regexp" 9 | "testing" 10 | ) 11 | 12 | func TestWithStackTrace(t *testing.T) { 13 | tests := []struct { 14 | err error 15 | want string 16 | }{ 17 | {err: Message("foo"), want: "foo"}, 18 | {err: io.EOF, want: "EOF"}, 19 | {err: nil, want: ""}, 20 | } 21 | for n, tt := range tests { 22 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 23 | err := WithStackTrace(tt.err, 0) 24 | if tt.err == nil { 25 | if err != nil { 26 | t.Errorf("WithStackTrace(nil): must return nil") 27 | } 28 | } else { 29 | if got := err.Error(); got != tt.want { 30 | t.Errorf("WithStackTrace(%#v).Error(): got: %q, want: %q", tt.err, got, tt.want) 31 | } 32 | if len(StackTrace(err)) == 0 { 33 | t.Errorf("WithStackTrace(%#v): returned error must contain a stack trace", tt.err) 34 | } 35 | if tt.err != nil { 36 | if !errors.Is(err, tt.err) { 37 | t.Errorf("errors.Is(WithStackTrace(%#v), err): must return true", tt.err) 38 | } 39 | if !errors.As(err, reflect.New(reflect.TypeOf(tt.err)).Interface()) { 40 | t.Errorf("errors.As(WithStackTrace(%#v), err): must return true", tt.err) 41 | } 42 | } 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func TestWithStackTraceFormat(t *testing.T) { 49 | tests := []struct { 50 | format string 51 | err error 52 | skip int 53 | want string 54 | regexp bool 55 | }{ 56 | {format: "%s", err: Message(""), want: ``}, 57 | {format: "%s", err: New("foo"), want: `foo`}, 58 | {format: "%s", err: io.EOF, want: `EOF`}, 59 | {format: "%v", err: New("foo"), want: `foo`}, 60 | {format: "%q", err: Message("foo"), want: `"foo"`}, 61 | } 62 | for n, tt := range tests { 63 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 64 | var err error 65 | func() { 66 | // We are running this in a closure to test stack trace frames 67 | // skipping. 68 | err = WithStackTrace(tt.err, tt.skip) 69 | }() 70 | if got := fmt.Sprintf(tt.format, err); got != tt.want { 71 | t.Errorf("fmt.Sprtinf(%q, WithStackTrace(%q)): got: %q, want: %q", tt.format, tt.err, got, tt.want) 72 | } 73 | }) 74 | } 75 | } 76 | 77 | func TestFrameFormat(t *testing.T) { 78 | frame := Frame{ 79 | File: "file", 80 | Line: 42, 81 | Function: "package/function", 82 | } 83 | tests := []struct { 84 | format string 85 | want string 86 | regexp bool 87 | }{ 88 | {format: "%s", want: "function (file:42)"}, 89 | {format: "%f", want: "file"}, 90 | {format: "%d", want: "42"}, 91 | {format: "%n", want: "function"}, 92 | {format: "%+n", want: "package/function"}, 93 | {format: "%+n", want: "package/function"}, 94 | {format: "%v", want: "function (file:42)"}, 95 | {format: "%+v", want: "{File:file Line:42 Function:package/function}"}, 96 | {format: "%#v", want: "xerrors._Frame{File:\"file\", Line:42, Function:\"package/function\"}"}, 97 | {format: "%q", want: "\"function (file:42)\""}, 98 | } 99 | for n, tt := range tests { 100 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 101 | if got := fmt.Sprintf(tt.format, frame); got != tt.want { 102 | t.Errorf("fmt.Sprtinf(%q, %#v): got: %q, want: %q", tt.format, frame, got, tt.want) 103 | } 104 | }) 105 | } 106 | } 107 | 108 | func TestCallersFormat(t *testing.T) { 109 | callers := callers(0) 110 | tests := []struct { 111 | format string 112 | want string 113 | }{ 114 | {format: "%s", want: `^at .*(\nat .*)+\n$`}, 115 | {format: "%v", want: `^at .*(\nat .*)+\n$`}, 116 | {format: "%+v", want: `\[([0-9 ])+\]`}, 117 | {format: "%#v", want: `^xerrors\._Callers\{(0x[a-f0-9]+, )*(0x[a-f0-9]+)\}$`}, 118 | {format: "%q", want: `^"at .*(\\nat .*)+\\n"$`}, 119 | } 120 | for n, tt := range tests { 121 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 122 | got := fmt.Sprintf(tt.format, callers) 123 | if match, _ := regexp.MatchString(tt.want, got); !match { 124 | t.Errorf("fmt.Sprtinf(%q, callers(0)): %q does not match %q", tt.format, got, tt.want) 125 | } 126 | }) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /stacktrace.go: -------------------------------------------------------------------------------- 1 | package xerrors 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "runtime" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // DefaultCallersFormatter is the default formatter for [Callers]. 12 | var DefaultCallersFormatter = func(c Callers, w io.Writer) { 13 | for _, frame := range c.Frames() { 14 | io.WriteString(w, "at ") 15 | frame.writeFrame(w) 16 | io.WriteString(w, "\n") 17 | } 18 | } 19 | 20 | // DefaultFrameFormatter is the default formatter for [Frame]. 21 | var DefaultFrameFormatter = func(f Frame, w io.Writer) { 22 | io.WriteString(w, shortname(f.Function)) 23 | io.WriteString(w, " (") 24 | io.WriteString(w, f.File) 25 | io.WriteString(w, ":") 26 | io.WriteString(w, strconv.Itoa(f.Line)) 27 | io.WriteString(w, ")") 28 | } 29 | 30 | const stackTraceDepth = 128 31 | 32 | // StackTrace extracts the stack trace from the provided error. 33 | // It traverses the error chain, looking for the last error that 34 | // has a stack trace. 35 | func StackTrace(err error) Callers { 36 | var callers Callers 37 | for err != nil { 38 | if st, ok := err.(interface{ StackTrace() Callers }); ok { 39 | callers = st.StackTrace() 40 | } 41 | if wErr, ok := err.(interface{ Unwrap() error }); ok { 42 | err = wErr.Unwrap() 43 | continue 44 | } 45 | break 46 | } 47 | return callers 48 | } 49 | 50 | // WithStackTrace wraps the provided error with a stack trace, 51 | // capturing the stack at the point of the call. The `skip` argument 52 | // specifies how many stack frames to skip. 53 | // 54 | // If err is nil, WithStackTrace returns nil. 55 | func WithStackTrace(err error, skip int) error { 56 | if err == nil { 57 | return nil 58 | } 59 | return &withStackTrace{ 60 | err: err, 61 | stack: callers(skip + 1), 62 | } 63 | } 64 | 65 | // withStackTrace wraps an error with a captured stack trace. 66 | type withStackTrace struct { 67 | err error 68 | stack Callers 69 | } 70 | 71 | // Error implements the [error] interface. 72 | func (e *withStackTrace) Error() string { 73 | return e.err.Error() 74 | } 75 | 76 | // ErrorDetails implements the [DetailedError] interface. 77 | func (e *withStackTrace) ErrorDetails() string { 78 | return e.stack.String() 79 | } 80 | 81 | // Unwrap implements the Go 1.13 `Unwrap() error` method, returning 82 | // the wrapped error. 83 | func (e *withStackTrace) Unwrap() error { 84 | return e.err 85 | } 86 | 87 | // StackTrace returns the stack trace captured at the point of the 88 | // error creation. 89 | func (e *withStackTrace) StackTrace() Callers { 90 | return e.stack 91 | } 92 | 93 | // Frame represents a single stack frame with file, line, and 94 | // function details. 95 | type Frame struct { 96 | File string 97 | Line int 98 | Function string 99 | } 100 | 101 | // String implements the [fmt.Stringer] interface. 102 | func (f Frame) String() string { 103 | s := &strings.Builder{} 104 | f.writeFrame(s) 105 | return s.String() 106 | } 107 | 108 | // Format implements the [fmt.Formatter] interface. 109 | // 110 | // Supported verbs: 111 | // - %s function, file, and line number in a single line 112 | // - %f filename 113 | // - %d line number 114 | // - %n function name, with '+' flag adding the package name 115 | // - %v same as %s; '+' or '#' flags print struct details 116 | // - %q double-quoted Go string, same as %s 117 | func (f Frame) Format(s fmt.State, verb rune) { 118 | type _Frame Frame 119 | switch verb { 120 | case 's': 121 | f.writeFrame(s) 122 | case 'f': 123 | io.WriteString(s, f.File) 124 | case 'd': 125 | io.WriteString(s, strconv.Itoa(f.Line)) 126 | case 'n': 127 | switch { 128 | case s.Flag('+'): 129 | io.WriteString(s, f.Function) 130 | default: 131 | io.WriteString(s, shortname(f.Function)) 132 | } 133 | case 'v': 134 | switch { 135 | case s.Flag('+') || s.Flag('#'): 136 | format(s, verb, _Frame(f)) 137 | default: 138 | f.Format(s, 's') 139 | } 140 | case 'q': 141 | io.WriteString(s, strconv.Quote(f.String())) 142 | default: 143 | format(s, verb, _Frame(f)) 144 | } 145 | } 146 | 147 | // writeFrame writes a formatted stack frame to the given [io.Writer]. 148 | func (f Frame) writeFrame(w io.Writer) { 149 | DefaultFrameFormatter(f, w) 150 | } 151 | 152 | // Callers represents a list of program counters from the 153 | // [runtime.Callers] function. 154 | type Callers []uintptr 155 | 156 | // Frames returns a slice of [Frame] structs with function, file, and 157 | // line information. 158 | func (c Callers) Frames() []Frame { 159 | r := make([]Frame, len(c)) 160 | f := runtime.CallersFrames(c) 161 | n := 0 162 | for { 163 | frame, more := f.Next() 164 | r[n] = Frame{ 165 | File: frame.File, 166 | Line: frame.Line, 167 | Function: frame.Function, 168 | } 169 | if !more { 170 | break 171 | } 172 | n++ 173 | } 174 | return r 175 | } 176 | 177 | // String implements the [fmt.Stringer] interface. 178 | func (c Callers) String() string { 179 | s := &strings.Builder{} 180 | c.writeTrace(s) 181 | return s.String() 182 | } 183 | 184 | // Format implements the [fmt.Formatter] interface. 185 | // 186 | // Supported verbs: 187 | // - %s complete stack trace 188 | // - %v same as %s; '+' or '#' flags print struct details 189 | // - %q double-quoted Go string, same as %s 190 | func (c Callers) Format(s fmt.State, verb rune) { 191 | type _Callers Callers 192 | switch verb { 193 | case 's': 194 | c.writeTrace(s) 195 | case 'v': 196 | switch { 197 | case s.Flag('+') || s.Flag('#'): 198 | format(s, verb, _Callers(c)) 199 | default: 200 | c.Format(s, 's') 201 | } 202 | case 'q': 203 | io.WriteString(s, strconv.Quote(c.String())) 204 | default: 205 | format(s, verb, _Callers(c)) 206 | } 207 | } 208 | 209 | // writeTrace writes the stack trace to the provided [io.Writer]. 210 | func (c Callers) writeTrace(w io.Writer) { 211 | DefaultCallersFormatter(c, w) 212 | } 213 | 214 | // callers captures the current stack trace, skipping the specified 215 | // number of frames. 216 | func callers(skip int) Callers { 217 | b := make([]uintptr, stackTraceDepth) 218 | l := runtime.Callers(skip+2, b[:]) 219 | return b[:l] 220 | } 221 | 222 | // shortname extracts the short name of a function, removing the 223 | // package path. 224 | func shortname(name string) string { 225 | i := strings.LastIndex(name, "/") 226 | return name[i+1:] 227 | } 228 | -------------------------------------------------------------------------------- /xerrors.go: -------------------------------------------------------------------------------- 1 | package xerrors 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // DetailedError represents an error that provides additional details 8 | // beyond the error message. 9 | type DetailedError interface { 10 | error 11 | 12 | // ErrorDetails returns additional details about the error. It should not 13 | // repeat the error message and should end with a newline. 14 | // 15 | // An empty string is returned if the error does not provide 16 | // additional details. 17 | ErrorDetails() string 18 | } 19 | 20 | // Message creates a simple error with the given message, without 21 | // recording a stack trace. Each call returns a distinct error 22 | // instance, even if the message is identical. 23 | // 24 | // This function is useful for creating sentinel errors, often 25 | // referred to as "constant errors." 26 | // 27 | // To create an error with a stack trace, use [New] or [Newf] 28 | // instead. 29 | func Message(msg string) error { 30 | return &messageError{msg: msg} 31 | } 32 | 33 | // Messagef creates a simple error with a formatted message, 34 | // without recording a stack trace. The format string follows the 35 | // conventions of [fmt.Sprintf]. Each call returns a distinct error 36 | // instance, even if the message is identical. 37 | // 38 | // This function is useful for creating sentinel errors, often 39 | // referred to as "constant errors." 40 | // 41 | // To create an error with a stack trace, use [New] or [Newf] 42 | // instead. 43 | func Messagef(format string, args ...any) error { 44 | return &messageError{msg: fmt.Sprintf(format, args...)} 45 | } 46 | 47 | // New creates a new error from the provided values and records a 48 | // stack trace at the point of the call. If multiple values are 49 | // provided, each value is wrapped by the previous one, forming a 50 | // chain of errors. 51 | // 52 | // Usage examples: 53 | // - Add a stack trace to an existing error: New(err) 54 | // - Create an error with a message and a stack trace: New("access denied") 55 | // - Wrap an error with a message: New("access denied", io.EOF) 56 | // - Add context to a sentinel error: New(ErrReadError, "access denied") 57 | // 58 | // Conversion rules for arguments: 59 | // - If the value is an error, it is used as is. 60 | // - If the value is a string, a new error with that message is 61 | // created. 62 | // - If the value implements [fmt.Stringer], the result of 63 | // String() is used to create an error. 64 | // - If the value is nil, it is ignored. 65 | // - Otherwise, the result of [fmt.Sprint] is used to create an 66 | // error. 67 | // 68 | // If called with no arguments or only nil values, New returns nil. 69 | // 70 | // To create a sentinel error, use [Message] or [Messagef] instead. 71 | func New(vals ...any) error { 72 | err := Join(vals...) 73 | if err == nil { 74 | return nil 75 | } 76 | return &withStackTrace{ 77 | err: err, 78 | stack: callers(1), 79 | } 80 | } 81 | 82 | // Newf creates a new error with a formatted message and records a 83 | // stack trace at the point of the call. The format string follows 84 | // the conventions of [fmt.Errorf]. 85 | // 86 | // Unlike errors created by [fmt.Errorf], the Unwrap method on the 87 | // returned error yields the next wrapped error, not a slice of errors, 88 | // since this function is intended for creating linear error chains. 89 | // 90 | // To create a sentinel error, use [Message] or [Messagef] instead. 91 | func Newf(format string, args ...any) error { 92 | return &withStackTrace{ 93 | err: Joinf(format, args...), 94 | stack: callers(1), 95 | } 96 | } 97 | 98 | // Join joins multiple values into a single error, forming a chain 99 | // of errors. 100 | // 101 | // Conversion rules for arguments: 102 | // - If the value is an error, it is used as is. 103 | // - If the value is a string, a new error with that message is 104 | // created. 105 | // - If the value implements [fmt.Stringer], the result of 106 | // String() is used to create an error. 107 | // - If the value is nil, it is ignored. 108 | // - Otherwise, the result of [fmt.Sprint] is used to create an 109 | // error. 110 | // 111 | // If called with no arguments or only nil values, Join returns nil. 112 | // 113 | // To create a multi-error instead of an error chain, use [Append]. 114 | func Join(vals ...any) error { 115 | var wErr error 116 | for i := len(vals) - 1; i >= 0; i-- { 117 | if vals[i] == nil { 118 | continue 119 | } 120 | err := toError(vals[i]) 121 | if wErr == nil { 122 | wErr = err 123 | continue 124 | } 125 | wErr = &withWrapper{ 126 | wrapper: err, 127 | err: wErr, 128 | } 129 | } 130 | return wErr 131 | } 132 | 133 | // Joinf joins multiple values into a single error with a formatted 134 | // message, forming an error chain. The format string follows the 135 | // conventions of [fmt.Errorf]. 136 | // 137 | // Unlike errors created by [fmt.Errorf], the Unwrap method on the 138 | // returned error yields the next wrapped error, not a slice of errors, 139 | // since this function is intended for creating linear error chains. 140 | // 141 | // To create a multi-error instead of an error chain, use [Append]. 142 | func Joinf(format string, args ...any) error { 143 | err := fmt.Errorf(format, args...) 144 | switch u := err.(type) { 145 | case interface { 146 | Unwrap() error 147 | }: 148 | return &withWrapper{ 149 | err: u.Unwrap(), 150 | msg: err.Error(), 151 | } 152 | case interface { 153 | Unwrap() []error 154 | }: 155 | var wErr error 156 | errs := u.Unwrap() 157 | for i := len(errs) - 1; i >= 0; i-- { 158 | if errs[i] == nil { 159 | continue 160 | } 161 | if wErr == nil { 162 | wErr = errs[i] 163 | continue 164 | } 165 | wErr = &withWrapper{ 166 | wrapper: errs[i], 167 | err: wErr, 168 | } 169 | } 170 | // Because the formatted message may not follow the "err1: err2: err3" 171 | // pattern, we set the msg field to overwrite the wrapper's message. 172 | if wErr, ok := wErr.(*withWrapper); ok { 173 | wErr.msg = err.Error() 174 | return wErr 175 | } 176 | // Edge case: if multiple %w verbs are used, and all of them are nil. 177 | if wErr == nil { 178 | return err 179 | } 180 | // Edge case: if multiple %w verbs are used, and only one of them is 181 | // not nil. 182 | return &withWrapper{ 183 | err: wErr, 184 | msg: err.Error(), 185 | } 186 | default: 187 | return &messageError{msg: err.Error()} 188 | } 189 | } 190 | 191 | // messageError represents a simple error that contains only a string 192 | // message. 193 | type messageError struct { 194 | msg string 195 | } 196 | 197 | // Error implements the [error] interface. 198 | func (e *messageError) Error() string { 199 | return e.msg 200 | } 201 | 202 | func toError(val any) error { 203 | var err error 204 | switch typ := val.(type) { 205 | case error: 206 | err = typ 207 | case string: 208 | err = &messageError{msg: typ} 209 | case fmt.Stringer: 210 | err = &messageError{msg: typ.String()} 211 | default: 212 | err = &messageError{msg: fmt.Sprint(val)} 213 | } 214 | return err 215 | } 216 | -------------------------------------------------------------------------------- /xerrors_test.go: -------------------------------------------------------------------------------- 1 | package xerrors 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | type stringer struct{ s string } 11 | 12 | func (s stringer) String() string { 13 | return s.s 14 | } 15 | 16 | func TestMessage(t *testing.T) { 17 | tests := []struct { 18 | val string 19 | want string 20 | }{ 21 | {val: "", want: ""}, 22 | {val: "foo", want: "foo"}, 23 | } 24 | for n, tt := range tests { 25 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 26 | got1 := Message(tt.val) 27 | got2 := Message(tt.val) 28 | if msg := got1.Error(); msg != tt.want { 29 | t.Errorf("Message(%#v): got: %q, want %q", tt.val, msg, tt.want) 30 | } 31 | if len(StackTrace(got1)) != 0 { 32 | t.Errorf("Message(%#v): returned error must not contain a stack trace", tt.val) 33 | } 34 | if got1 == got2 { 35 | t.Errorf("Message(%#v): returned error must not be the same instance", tt.val) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | func TestMessagef(t *testing.T) { 42 | tests := []struct { 43 | format string 44 | args []any 45 | want string 46 | }{ 47 | {format: "", args: nil, want: ""}, 48 | {format: "foo", args: nil, want: "foo"}, 49 | {format: "foo %d", args: []any{42}, want: "foo 42"}, 50 | } 51 | for n, tt := range tests { 52 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 53 | got1 := Messagef(tt.format, tt.args...) 54 | got2 := Messagef(tt.format, tt.args...) 55 | if msg := got1.Error(); msg != tt.want { 56 | t.Errorf("Messagef(%q, %#v): got: %q, want %q", tt.format, tt.args, msg, tt.want) 57 | } 58 | if len(StackTrace(got1)) != 0 { 59 | t.Errorf("Messagef(%q, %#v): returned error must not contain a stack trace", tt.format, tt.args) 60 | } 61 | if got1 == got2 { 62 | t.Errorf("Messagef(%q, %#v): returned error must not be the same instance", tt.format, tt.args) 63 | } 64 | }) 65 | } 66 | } 67 | 68 | func TestNew(t *testing.T) { 69 | // Since New is mostly a wrapper around Join, we only test 70 | // the error message and stack trace. 71 | tests := []struct { 72 | vals []any 73 | want string 74 | wantNil bool 75 | }{ 76 | {vals: []any{"foo", "bar"}, want: "foo: bar"}, 77 | {vals: []any{nil}, wantNil: true}, 78 | } 79 | for n, tt := range tests { 80 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 81 | got := New(tt.vals...) 82 | switch { 83 | case tt.wantNil: 84 | if got != nil { 85 | t.Errorf("New(%#v): expected nil", tt.vals) 86 | } 87 | default: 88 | if got.Error() != tt.want { 89 | t.Errorf("New(%#v): got: %q, want %q", tt.vals, got, tt.want) 90 | } 91 | st := StackTrace(got) 92 | if len(st) == 0 { 93 | t.Errorf("New(%#v): returned error must contain a stack trace", tt.vals) 94 | return 95 | } 96 | if !strings.Contains(st.Frames()[0].Function, "TestNew") { 97 | t.Errorf("New(%#v): first frame must point to TestNew", tt.vals) 98 | } 99 | } 100 | }) 101 | } 102 | } 103 | 104 | func TestNewf(t *testing.T) { 105 | // Since Newf is mostly a wrapper around Joinf, we only test 106 | // the error message and stack trace. 107 | tests := []struct { 108 | format string 109 | args []any 110 | want string 111 | }{ 112 | {format: "foo", args: nil, want: "foo"}, 113 | } 114 | for n, tt := range tests { 115 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 116 | got := Newf(tt.format, tt.args...) 117 | if got.Error() != tt.want { 118 | t.Errorf("Newf(%q, %#v): got: %q, want %q", tt.format, tt.args, got, tt.want) 119 | } 120 | st := StackTrace(got) 121 | if len(st) == 0 { 122 | t.Errorf("Newf(%q, %#v): returned error must contain a stack trace", tt.format, tt.args) 123 | return 124 | } 125 | if !strings.Contains(st.Frames()[0].Function, "TestNewf") { 126 | t.Errorf("Newf(%q, %#v): first frame must point to TestNewf", tt.format, tt.args) 127 | } 128 | }) 129 | } 130 | } 131 | 132 | func TestJoin(t *testing.T) { 133 | tests := []struct { 134 | vals []any 135 | want string 136 | wantNil bool 137 | }{ 138 | // String 139 | {vals: []any{""}, want: ""}, 140 | {vals: []any{"foo", "bar"}, want: "foo: bar"}, 141 | 142 | // Error 143 | {vals: []any{Message("foo"), Message("bar")}, want: "foo: bar"}, 144 | 145 | // Stringer 146 | {vals: []any{stringer{s: "foo"}, stringer{s: "bar"}}, want: "foo: bar"}, 147 | 148 | // Sprintf 149 | {vals: []any{42, 314}, want: "42: 314"}, 150 | 151 | // Nil cases 152 | {vals: []any{}, wantNil: true}, 153 | {vals: []any{nil}, wantNil: true}, 154 | {vals: []any{nil, nil}, wantNil: true}, 155 | {vals: []any{nil, "foo", "bar"}, want: "foo: bar"}, 156 | {vals: []any{"foo", nil, "bar"}, want: "foo: bar"}, 157 | } 158 | for n, tt := range tests { 159 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 160 | got := Join(tt.vals...) 161 | switch { 162 | case tt.wantNil: 163 | if got != nil { 164 | t.Errorf("Join(%#v): expected nil", tt.vals) 165 | } 166 | default: 167 | if got.Error() != tt.want { 168 | t.Errorf("Join(%#v): got: %q, want %q", tt.vals, got, tt.want) 169 | } 170 | if len(StackTrace(got)) != 0 { 171 | t.Errorf("Join(%#v): returned error must not contain a stack trace", tt.vals) 172 | } 173 | for _, v := range tt.vals { 174 | if err, ok := v.(error); ok { 175 | if !errors.Is(got, err) { 176 | t.Errorf("errors.Is(Join(errs...), err): must return true") 177 | } 178 | } 179 | } 180 | } 181 | }) 182 | } 183 | } 184 | 185 | func TestJoinf(t *testing.T) { 186 | err := Message("first error") 187 | tests := []struct { 188 | format string 189 | args []any 190 | want string 191 | }{ 192 | {format: "simple error", args: nil, want: "simple error"}, 193 | {format: "error with value %d", args: []any{42}, want: "error with value 42"}, 194 | {format: "wrapped error: %w", args: []any{err}, want: "wrapped error: first error"}, 195 | {format: "wrapped nil error: %w", args: []any{nil}, want: "wrapped nil error: %!w()"}, 196 | } 197 | for n, tt := range tests { 198 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 199 | got := Joinf(tt.format, tt.args...) 200 | if got == nil { 201 | t.Errorf("Joinf(%q, %#v): expected non-nil error", tt.format, tt.args) 202 | return 203 | } 204 | if got.Error() != tt.want { 205 | t.Errorf("Joinf(%q, %#v): got: %q, want %q", tt.format, tt.args, got, tt.want) 206 | } 207 | if len(StackTrace(got)) != 0 { 208 | t.Errorf("Joinf(%q, %#v): returned error must not contain a stack trace", tt.format, tt.args) 209 | } 210 | for _, v := range tt.args { 211 | if err, ok := v.(error); ok { 212 | if !errors.Is(got, err) { 213 | t.Errorf("errors.Is(Joinf(errs...), err): must return true") 214 | } 215 | } 216 | } 217 | }) 218 | } 219 | } 220 | 221 | func TestJoin_Unwrap(t *testing.T) { 222 | err1 := Message("first error") 223 | err2 := Message("second error") 224 | got := Join(err1, err2) 225 | unwrapper, ok := got.(interface{ Unwrap() error }) 226 | if !ok { 227 | t.Fatalf("Join(err1, err2) must implement Unwrap()") 228 | } 229 | unwrapped := unwrapper.Unwrap() 230 | if unwrapped == nil { 231 | t.Fatalf("Join(err1, err2).Unwrap() must not return nil") 232 | } 233 | if errors.Is(unwrapped, err1) || !errors.Is(unwrapped, err2) { 234 | t.Fatalf("Join(err1, err2).Unwrap() must return the second error") 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-xerrors 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/mdobak/go-xerrors.svg)](https://pkg.go.dev/github.com/mdobak/go-xerrors) [![Go Report Card](https://goreportcard.com/badge/github.com/mdobak/go-xerrors)](https://goreportcard.com/report/github.com/mdobak/go-xerrors) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Coverage Status](https://coveralls.io/repos/github/MDobak/go-xerrors/badge.svg?branch=coverage)](https://coveralls.io/github/MDobak/go-xerrors?branch=coverage) 4 | 5 | `go-xerrors` is a small, idiomatic library that makes error handling in Go easier. It provides utilities for creating errors with stack traces, wrapping existing errors, aggregating multiple errors, and recovering from panics. 6 | 7 | **Main features** 8 | 9 | - **Stack traces**: Capture stack traces when creating errors to pinpoint the source during debugging. 10 | - **Multi-errors**: Aggregate multiple errors into a single error while preserving individual context. 11 | - **Panic handling**: Convert panic values to standard Go errors with stack traces. 12 | - **Zero dependencies**: No external dependencies beyond the Go standard library. 13 | 14 | > Note: This package is stable. Updates are rare and limited to bug fixes and support for new Go versions or error-related features. Since 1.0, the API has been frozen. No breaking changes will be introduced in future releases. 15 | 16 | --- 17 | 18 | ## Installation 19 | 20 | ```bash 21 | go get -u github.com/mdobak/go-xerrors 22 | ``` 23 | 24 | ## Usage 25 | 26 | ### Example 27 | 28 | Here’s a quick example of creating and handling errors with `go-xerrors`. 29 | 30 | ```go 31 | package main 32 | 33 | import ( 34 | "database/sql" 35 | "fmt" 36 | 37 | "github.com/mdobak/go-xerrors" 38 | ) 39 | 40 | func findUserByID(id int) error { 41 | // Simulate a standard library error. 42 | err := sql.ErrNoRows 43 | 44 | // Wrap the original error with additional context and capture a stack trace 45 | // at this point in the call stack. 46 | return xerrors.Newf("user %d not found: %w", id, err) 47 | } 48 | 49 | func main() { 50 | err := findUserByID(123) 51 | if err != nil { 52 | // 1) err.Error() returns a concise, log-friendly message. 53 | fmt.Println(err.Error()) 54 | // Output: 55 | // user 123 not found: sql: no rows in result set 56 | 57 | // 2) xerrors.Print shows a detailed message with a stack trace. 58 | xerrors.Print(err) 59 | // Output: 60 | // Error: user 123 not found: sql: no rows in result set 61 | // at main.findUserByID (/home/user/app/main.go:15) 62 | // at main.main (/home/user/app/main.go:20) 63 | // at runtime.main (/usr/local/go/src/runtime/proc.go:250) 64 | // at runtime.goexit (/usr/local/go/src/runtime/asm_amd64.s:1594) 65 | } 66 | } 67 | ``` 68 | 69 | ### Creating Errors with Stack Traces 70 | 71 | The primary way to create an error in `go-xerrors` is by using the `xerrors.New` or `xerrors.Newf` functions: 72 | 73 | ```go 74 | // Create a new error with a stack trace. 75 | err := xerrors.New("something went wrong") 76 | 77 | // Create a formatted error with a stack trace. 78 | err := xerrors.Newf("something went wrong: %s", reason) 79 | ``` 80 | 81 | Calling `err.Error()` returns only the message ("something went wrong"), following the Go convention of keeping error strings concise. 82 | 83 | ### Displaying Detailed Errors 84 | 85 | To print the error with its stack trace and details, use `xerrors.Print`, `xerrors.Sprint`, or `xerrors.Fprint`: 86 | 87 | ```go 88 | xerrors.Print(err) 89 | ``` 90 | 91 | Output: 92 | 93 | ``` 94 | Error: something went wrong 95 | at main.main (/home/user/app/main.go:10) 96 | at runtime.main (/usr/local/go/src/runtime/proc.go:225) 97 | at runtime.goexit (/usr/local/go/src/runtime/asm_amd64.s:1371) 98 | ``` 99 | 100 | ### Working with Stack Traces 101 | 102 | To retrieve the stack trace information programmatically: 103 | 104 | ```go 105 | trace := xerrors.StackTrace(err) 106 | fmt.Print(trace) 107 | ``` 108 | 109 | Output: 110 | 111 | ``` 112 | at main.TestMain (/home/user/app/main_test.go:10) 113 | at testing.tRunner (/home/user/go/src/testing/testing.go:1259) 114 | at runtime.goexit (/home/user/go/src/runtime/asm_arm64.s:1133) 115 | ``` 116 | 117 | You can also add a stack trace to an existing error with `xerrors.WithStackTrace`, and choose how many frames to skip. This is handy when a helper creates errors but you don’t want its own frame captured: 118 | 119 | ```go 120 | func errNotFound(path string) error { 121 | return xerrors.WithStackTrace(ErrNotFound{path: path}, 1) // skip one frame 122 | } 123 | ``` 124 | 125 | ### Wrapping Errors 126 | 127 | You can also wrap existing errors: 128 | 129 | ```go 130 | output, err := json.Marshal(data) 131 | if err != nil { 132 | return xerrors.New("failed to marshal data", err) 133 | } 134 | ``` 135 | 136 | With formatted messages: 137 | 138 | ```go 139 | output, err := json.Marshal(data) 140 | if err != nil { 141 | return xerrors.Newf("failed to marshal data %v: %w", data, err) 142 | } 143 | ``` 144 | 145 | > Wrapping multiple errors with `xerrors.Newf` is possible only in Go 1.20 and later. 146 | 147 | ### Creating Error Chains Without Stack Traces 148 | 149 | When you don’t need a stack trace (for example, when creating sentinel errors), use `xerrors.Join` and `xerrors.Joinf`: 150 | 151 | ```go 152 | err := xerrors.Join("operation failed", otherErr) 153 | ``` 154 | 155 | With formatted messages: 156 | 157 | ```go 158 | err := xerrors.Joinf("operation failed: %w", otherErr) 159 | ``` 160 | 161 | > Wrapping multiple errors with `xerrors.Joinf` is possible only in Go 1.20 and later. 162 | 163 | The main difference between Go's `fmt.Errorf` and `xerrors.Newf`/`xerrors.Joinf` is that the latter functions preserve the error chain, whereas `fmt.Errorf` flattens it (i.e., its `Unwrap` method returns all underlying errors at once instead of just the next one in the chain). 164 | 165 | ### Sentinel Errors 166 | 167 | Sentinel errors are predefined, exported error values used to signal specific, well-known conditions (e.g., `io.EOF`). The `go-xerrors` package provides the `xerrors.Message` and `xerrors.Messagef` functions to create distinct sentinel error values: 168 | 169 | ```go 170 | var ErrAccessDenied = xerrors.Message("access denied") 171 | 172 | // ... 173 | 174 | func performAction() error { 175 | // ... 176 | return ErrAccessDenied 177 | } 178 | 179 | // ... 180 | 181 | err := performAction() 182 | if errors.Is(err, ErrAccessDenied) { 183 | log.Println("Operation failed due to access denial.") 184 | } 185 | ``` 186 | 187 | For formatted sentinel errors: 188 | 189 | ```go 190 | const MaxLength = 10 191 | var ErrInvalidInput = xerrors.Messagef("max length of %d exceeded", MaxLength) 192 | ``` 193 | 194 | ### Multi-Errors 195 | 196 | When performing multiple independent operations where several might fail, use `xerrors.Append` to collect these individual errors into a single multi-error instance: 197 | 198 | ```go 199 | var err error 200 | 201 | if input.Username == "" { 202 | err = xerrors.Append(err, xerrors.New("username cannot be empty")) 203 | } 204 | if len(input.Password) < 8 { 205 | err = xerrors.Append(err, xerrors.New("password must be at least 8 characters")) 206 | } 207 | 208 | if err != nil { 209 | fmt.Println(err.Error()) 210 | // Output: 211 | // [username cannot be empty, password must be at least 8 characters] 212 | 213 | // Detailed output using xerrors.Print: 214 | xerrors.Print(err) 215 | // Output: 216 | // Error: [username cannot be empty, password must be at least 8 characters] 217 | // 1. Error: username cannot be empty 218 | // at main.validateInput (/home/user/app/main.go:40) 219 | // at main.main (/home/user/app/main.go:20) 220 | // at runtime.main (/usr/local/go/src/runtime/proc.go:250) 221 | // at runtime.goexit (/usr/local/go/src/runtime/asm_amd64.s:1594) 222 | // 2. Error: password must be at least 8 characters 223 | // at main.validateInput (/home/user/app/main.go:43) 224 | // at main.main (/home/user/app/main.go:20) 225 | // at runtime.main (/usr/local/go/src/runtime/proc.go:250) 226 | // at runtime.goexit (/usr/local/go/src/runtime/asm_amd64.s:1594) 227 | } 228 | ``` 229 | 230 | The resulting multi-error implements the standard `error` interface as well as `errors.Is`, `errors.As`, and `errors.Unwrap`, allowing you to check for specific errors or extract them. 231 | 232 | **Comparison with Go 1.20 `errors.Join`:** 233 | 234 | Go 1.20 introduced `errors.Join` for error aggregation. While it serves a similar purpose, `xerrors.Append` preserves the individual stack traces associated with each appended error and adheres to the convention of returning a single line from the `Error()` method. 235 | 236 | ### Simplified Panic Handling 237 | 238 | Panics can be challenging to locate and handle effectively in Go applications, especially when using `recover()`. Common issues, such as nil pointer dereferences or out-of-bounds slice accesses, often result in unclear panic messages. Without a stack trace, pinpointing the origin of the panic can be difficult. 239 | 240 | `go-xerrors` provides utilities to convert panic values into proper errors with stack traces. 241 | 242 | **Using `xerrors.Recover`:** 243 | 244 | ```go 245 | func handleTask() (err error) { 246 | defer xerrors.Recover(func(err error) { 247 | log.Printf("Recovered from panic during task handling: %s", xerrors.Sprint(err)) 248 | }) 249 | 250 | // ... potentially panicking code ... 251 | 252 | return nil 253 | } 254 | ``` 255 | 256 | **Using `xerrors.FromRecover`:** 257 | 258 | ```go 259 | func handleTask() (err error) { 260 | defer func() { 261 | if r := recover(); r != nil { 262 | err = xerrors.FromRecover(r) // Convert recovered value to error with stack trace 263 | log.Printf("Recovered from panic during task handling: %s", xerrors.Sprint(err)) 264 | } 265 | }() 266 | 267 | // ... potentially panicking code ... 268 | 269 | return nil 270 | } 271 | ``` 272 | 273 | The returned error implements the `PanicError` interface, which provides access to the original panic value via the `Panic()` method. 274 | 275 | ### Choosing Between `New`, `Join`, and `Append` 276 | 277 | While these functions can all be used to aggregate errors, they each serve distinct purposes: 278 | 279 | - **`xerrors.New`**: Use this to create errors and attach stack traces, especially when wrapping existing errors to provide additional context. 280 | - **`xerrors.Join`**: Use this to chain errors together _without_ capturing stack traces. 281 | - **`xerrors.Append`**: Use this to aggregate multiple, independent errors into a single multi-error. This is useful when several operations might fail, and you want to report all failures at once. 282 | 283 | #### Examples 284 | 285 | ##### Error with Stack Trace 286 | 287 | ```go 288 | func (m *MyStruct) MarshalJSON() ([]byte, error) { 289 | output, err := json.Marshal(m) 290 | if err != nil { 291 | // Wrap the error with additional context and capture a stack trace. 292 | return nil, xerrors.New("failed to marshal data", err) 293 | } 294 | return output, nil 295 | } 296 | ``` 297 | 298 | ##### Sentinel Errors 299 | 300 | ```go 301 | var ( 302 | // Using xerrors.Join allows us to create sentinel errors that can be 303 | // checked with errors.Is against both ErrValidation and the 304 | // specific validation error. We do not want to capture a stack trace 305 | // here; therefore, we use xerrors.Join instead of xerrors.New. 306 | ErrValidation = xerrors.Message("validation error") 307 | ErrInvalidName = xerrors.Join(ErrValidation, "name is invalid") 308 | ErrInvalidAge = xerrors.Join(ErrValidation, "age is invalid") 309 | ErrInvalidEmail = xerrors.Join(ErrValidation, "email is invalid") 310 | ) 311 | 312 | func (m *MyStruct) Validate() error { 313 | if !m.isNameValid() { 314 | return xerrors.New(ErrInvalidName) 315 | } 316 | if !m.isAgeValid() { 317 | return xerrors.New(ErrInvalidAge) 318 | } 319 | if !m.isEmailValid() { 320 | return xerrors.New(ErrInvalidEmail) 321 | } 322 | return nil 323 | } 324 | ``` 325 | 326 | ##### Multi-Error Validation 327 | 328 | ```go 329 | func (m *MyStruct) Validate() error { 330 | var err error 331 | if m.Name == "" { 332 | err = xerrors.Append(err, xerrors.New("name cannot be empty")) 333 | } 334 | if m.Age < 0 { 335 | err = xerrors.Append(err, xerrors.New("age cannot be negative")) 336 | } 337 | if m.Email == "" { 338 | err = xerrors.Append(err, xerrors.New("email cannot be empty")) 339 | } 340 | return err 341 | } 342 | ``` 343 | 344 | ## API Reference 345 | 346 | ### Core Functions 347 | 348 | - `xerrors.New(errors ...any) error`: Creates a new error with a stack trace 349 | - `xerrors.Newf(format string, args ...any) error`: Creates a formatted error with a stack trace 350 | - `xerrors.Join(errors ...any) error`: Creates a chained error without a stack trace 351 | - `xerrors.Joinf(format string, args ...any) error`: Creates a formatted chained error without a stack trace 352 | - `xerrors.Message(message string) error`: Creates a simple sentinel error 353 | - `xerrors.Messagef(format string, args ...any) error`: Creates a simple formatted sentinel error 354 | - `xerrors.Append(err error, errs ...error) error`: Aggregates errors into a multi-error 355 | 356 | ### Panics 357 | 358 | - `xerrors.Recover(callback func(err error))`: Recovers from panics and invokes a callback with the error 359 | - `xerrors.FromRecover(recoveredValue any) error`: Converts a recovered value to an error with a stack trace 360 | 361 | ### Stack Trace 362 | 363 | - `xerrors.StackTrace(err error) Callers`: Extracts the stack trace from an error 364 | - `xerrors.WithStackTrace(err error, skip int) error`: Wraps an error with a stack trace 365 | - `DefaultCallersFormatter`: The default formatter for Callers, used when printing stack traces. 366 | - `DefaultFrameFormatter`: The default formatter for Frame, used when printing stack traces. 367 | 368 | ### Error printing 369 | 370 | - `xerrors.Print(err error)`: Prints a formatted error to stderr 371 | - `xerrors.Sprint(err error) string`: Returns a formatted error as a string 372 | - `xerrors.Fprint(w io.Writer, err error)`: Writes a formatted error to the provided writer 373 | 374 | ### Interfaces 375 | 376 | - `DetailedError`: For errors that provide detailed information 377 | - `PanicError`: For errors created from panic values with access to the original panic value 378 | 379 | ## Documentation 380 | 381 | For full API details, see the documentation: 382 | 383 | [https://pkg.go.dev/github.com/mdobak/go-xerrors](https://pkg.go.dev/github.com/mdobak/go-xerrors) 384 | 385 | ## License 386 | 387 | Licensed under the MIT License. 388 | --------------------------------------------------------------------------------