├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── benchmark └── stacktrace_benchmark_test.go ├── builder.go ├── builder_test.go ├── common.go ├── error.go ├── error_112.go ├── error_113.go ├── error_113_test.go ├── error_test.go ├── example_test.go ├── go.mod ├── go.sum ├── helper.go ├── id.go ├── modifier.go ├── modifier_test.go ├── namespace.go ├── namespace_test.go ├── panic.go ├── panic_test.go ├── property.go ├── property_test.go ├── readme.go ├── registry.go ├── registry_test.go ├── stackframe.go ├── stacktrace.go ├── stacktrace_test.go ├── switch.go ├── switch_test.go ├── trait.go ├── trait_test.go ├── type.go ├── type_test.go ├── utils.go ├── utils_test.go ├── wrap.go └── wrap_test.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | go: [1.11, 1.12, 1.13, 1.14, 1.15, 1.16, 1.17, 1.18, 1.19, '1.20', 1.21, 1.22, 1.23] 13 | 14 | steps: 15 | 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | 19 | - name: Set up Go ${{ matrix.go }} 20 | uses: actions/setup-go@v1 21 | with: 22 | go-version: ${{ matrix.go }} 23 | 24 | - name: Build 25 | run: go test -v ./... 26 | 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # IDE 15 | .idea/ 16 | *.iml 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | before_script: if [[ $TRAVIS_GO_VERSION =~ (^1\.8) ]]; then cd $GOPATH/src/github.com/stretchr/testify/ && git checkout v1.2.2 && cd -; fi 4 | 5 | go: 6 | - "1.8.x" 7 | - "1.11.x" 8 | - "1.12.x" 9 | - "1.13.x" 10 | - "1.14.x" 11 | - "1.15.x" 12 | - master 13 | 14 | env: 15 | global: 16 | - GO111MODULE=on 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Joom 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Github Actions Build Status](https://github.com/joomcode/errorx/workflows/CI/badge.svg)](https://github.com/joomcode/errorx/actions) 2 | [![GoDoc](https://img.shields.io/static/v1?label=godev&message=reference&color=00add8)](https://pkg.go.dev/github.com/joomcode/errorx?tab=doc) 3 | [![Report Card](https://goreportcard.com/badge/github.com/joomcode/errorx)](https://goreportcard.com/report/github.com/joomcode/errorx) 4 | [![gocover.io](https://gocover.io/_badge/github.com/joomcode/errorx)](https://gocover.io/github.com/joomcode/errorx) 5 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go#error-handling) 6 | 7 | ## Highlights 8 | 9 | The *errorx* library provides error implementation and error-related utilities. 10 | Library features include (but are not limited to): 11 | * Stack traces 12 | * Composability of errors 13 | * Means to enhance error both with stack trace and with message 14 | * Robust type and trait checks 15 | 16 | ## Introduction 17 | 18 | Conventional approach towards errors in *Go* is quite limited. 19 | 20 | The typical case implies an error being created at some point: 21 | ```go 22 | return errors.New("now this is unfortunate") 23 | ``` 24 | 25 | Then being passed along with a no-brainer: 26 | ```go 27 | if err != nil { 28 | return err 29 | } 30 | ``` 31 | 32 | And, finally, handled by printing it to the log file: 33 | ```go 34 | log.Printf("Error: %s", err) 35 | ``` 36 | 37 | It doesn't take long to find out that quite often this is not enough. There's little fun in solving the issue when everything a developer is able to observe is a line in the log that looks like one of those: 38 | > Error: EOF 39 | 40 | > Error: unexpected '>' at the beginning of value 41 | 42 | > Error: wrong argument value 43 | 44 | An *errorx* library makes an approach to create a toolset that would help remedy this issue with these considerations in mind: 45 | * No extra care should be required for an error to have all the necessary debug information; it is the opposite that may constitute a special case 46 | * There must be a way to distinguish one kind of error from another, as they may imply or require a different handling in user code 47 | * Errors must be composable, and patterns like ```if err == io.EOF``` defeat that purpose, so they should be avoided 48 | * Some context information may be added to the error along the way, and there must be a way to do so without altering the semantics of the error 49 | * It must be easy to create an error, add some context to it, check for it 50 | * A kind of error that requires a special treatment by the caller *is* a part of a public API; an excessive amount of such kinds is a code smell 51 | 52 | As a result, the goal of the library is to provide a brief, expressive syntax for a conventional error handling and to discourage usage patterns that bring more harm than they're worth. 53 | 54 | Error-related, negative codepath is typically less well tested, though of, and may confuse the reader more than its positive counterpart. Therefore, an error system could do well without too much of a flexibility and unpredictability. 55 | 56 | # errorx 57 | 58 | With *errorx*, the pattern above looks like this: 59 | 60 | ```go 61 | return errorx.IllegalState.New("unfortunate") 62 | ``` 63 | ```go 64 | if err != nil { 65 | return errorx.Decorate(err, "this could be so much better") 66 | } 67 | ``` 68 | ```go 69 | log.Printf("Error: %+v", err) 70 | ``` 71 | 72 | An error message will look something like this: 73 | 74 | ``` 75 | Error: this could be so much better, cause: common.illegal_state: unfortunate 76 | at main.culprit() 77 | main.go:21 78 | at main.innocent() 79 | main.go:16 80 | at main.main() 81 | main.go:11 82 | ``` 83 | 84 | Now we have some context to our little problem, as well as a full stack trace of the original cause - which is, in effect, all that you really need, most of the time. ```errorx.Decorate``` is handy to add some info which a stack trace does not already hold: an id of the relevant entity, a portion of the failed request, etc. In all other cases, the good old ```if err != nil {return err}``` still works for you. 85 | 86 | And this, frankly, may be quite enough. With a set of standard error types provided with *errorx* and a syntax to create your own (note that a name of the type is a good way to express its semantics), the best way to deal with errors is in an opaque manner: create them, add information and log as some point. Whenever this is sufficient, don't go any further. The simpler, the better. 87 | 88 | ## Error check 89 | 90 | If an error requires special treatment, it may be done like this: 91 | ```go 92 | // MyError = MyErrors.NewType("my_error") 93 | if errorx.IsOfType(err, MyError) { 94 | // handle 95 | } 96 | ``` 97 | 98 | Note that it is never a good idea to inspect a message of an error. Type check, on the other hand, is sometimes OK, especially if this technique is used inside of a package rather than forced upon API users. 99 | 100 | An alternative is a mechanisms called **traits**: 101 | ```go 102 | // the first parameter is a name of new error type, the second is a reference to existing trait 103 | TimeoutElapsed = MyErrors.NewType("timeout", errorx.Timeout()) 104 | ``` 105 | 106 | Here, ```TimeoutElapsed``` error type is created with a Timeout() trait, and errors may be checked against it: 107 | ```go 108 | if errorx.HasTrait(err, errorx.Timeout()) { 109 | // handle 110 | } 111 | ``` 112 | 113 | Note that here a check is made against a trait, not a type, so any type with the same trait would pass it. Type check is more restricted this way and creates tighter dependency if used outside of an originating package. It allows for some little flexibility, though: via a subtype feature a broader type check can be made. 114 | 115 | ## Wrap 116 | 117 | The example above introduced ```errorx.Decorate()```, a syntax used to add message as an error is passed along. This mechanism is highly non-intrusive: any properties an original error possessed, a result of a ```Decorate()``` will possess, too. 118 | 119 | Sometimes, though, it is not the desired effect. A possibility to make a type check is a double edged one, and should be restricted as often as it is allowed. The bad way to do so would be to create a new error and to pass an ```Error()``` output as a message. Among other possible issues, this would either lose or duplicate the stack trace information. 120 | 121 | A better alternative is: 122 | ```go 123 | return MyError.Wrap(err, "fail") 124 | ``` 125 | 126 | With ```Wrap()```, an original error is fully retained for the log, but hidden from type checks by the caller. 127 | 128 | See ```WrapMany()``` and ```DecorateMany()``` for more sophisticated cases. 129 | 130 | ## Stack traces 131 | 132 | As an essential part of debug information, stack traces are included in all *errorx* errors by default. 133 | 134 | When an error is passed along, the original stack trace is simply retained, as this typically takes place along the lines of the same frames that were originally captured. When an error is received from another goroutine, use this to add frames that would otherwise be missing: 135 | 136 | ```go 137 | return errorx.EnhanceStackTrace(<-errorChan, "task failed") 138 | ``` 139 | 140 | Result would look like this: 141 | ``` 142 | Error: task failed, cause: common.illegal_state: unfortunate 143 | at main.proxy() 144 | main.go:17 145 | at main.main() 146 | main.go:11 147 | ---------------------------------- 148 | at main.culprit() 149 | main.go:26 150 | at main.innocent() 151 | main.go:21 152 | ``` 153 | 154 | On the other hand, some errors do not require a stack trace. Some may be used as a control flow mark, other are known to be benign. Stack trace could be omitted by not using the ```%+v``` formatting, but the better alternative is to modify the error type: 155 | 156 | ```go 157 | ErrInvalidToken = AuthErrors.NewType("invalid_token").ApplyModifiers(errorx.TypeModifierOmitStackTrace) 158 | ``` 159 | 160 | This way, a receiver of an error always treats it the same way, and it is the producer who modifies the behaviour. Following, again, the principle of opacity. 161 | 162 | Other relevant tools include ```EnsureStackTrace(err)``` to provide an error of unknown nature with a stack trace, if it lacks one. 163 | 164 | ### Stack traces benchmark 165 | 166 | As performance is obviously an issue, some measurements are in order. The benchmark is provided with the library. In all of benchmark cases, a very simple code is called that does nothing but grows a number of frames and immediately returns an error. 167 | 168 | Result sample, MacBook Pro Intel Core i7-6920HQ CPU @ 2.90GHz 4 core: 169 | 170 | name | runs | ns/op | note 171 | ------ | ------: | ------: | ------ 172 | BenchmarkSimpleError10 | 20000000 | 57.2 | simple error, 10 frames deep 173 | BenchmarkErrorxError10 | 10000000 | 138 | same with errorx error 174 | BenchmarkStackTraceErrorxError10 | 1000000 | 1601 | same with collected stack trace 175 | BenchmarkSimpleError100 | 3000000 | 421 | simple error, 100 frames deep 176 | BenchmarkErrorxError100 | 3000000 | 507 | same with errorx error 177 | BenchmarkStackTraceErrorxError100 | 300000 | 4450 | same with collected stack trace 178 | BenchmarkStackTraceNaiveError100-8 | 2000 | 588135 | same with naive debug.Stack() error implementation 179 | BenchmarkSimpleErrorPrint100 | 2000000 | 617 | simple error, 100 frames deep, format output 180 | BenchmarkErrorxErrorPrint100 | 2000000 | 935 | same with errorx error 181 | BenchmarkStackTraceErrorxErrorPrint100 | 30000 | 58965 | same with collected stack trace 182 | BenchmarkStackTraceNaiveErrorPrint100-8 | 2000 | 599155 | same with naive debug.Stack() error implementation 183 | 184 | Key takeaways: 185 | * With deep enough call stack, trace capture brings **10x slowdown** 186 | * This is an absolute **worst case measurement, no-op function**; in a real life, much more time is spent doing actual work 187 | * Then again, in real life code invocation does not always result in error, so the overhead is proportional to the % of error returns 188 | * Still, it pays to omit stack trace collection when it would be of no use 189 | * It is actually **much more expensive to format** an error with a stack trace than to create it, roughly **another 10x** 190 | * Compared to the most naive approach to stack trace collection, error creation it is **100x** cheaper with errorx 191 | * Therefore, it is totally OK to create an error with a stack trace that would then be handled and not printed to log 192 | * Realistically, stack trace overhead is only painful either if a code is very hot (called a lot and returns errors often) or if an error is used as a control flow mechanism and does not constitute an actual problem; in both cases, stack trace should be omitted 193 | 194 | ## More 195 | 196 | See [godoc](https://godoc.org/github.com/joomcode/errorx) for other *errorx* features: 197 | * Namespaces 198 | * Type switches 199 | * ```errorx.Ignore``` 200 | * Trait inheritance 201 | * Dynamic properties 202 | * Panic-related utils 203 | * Type registry 204 | * etc. 205 | -------------------------------------------------------------------------------- /benchmark/stacktrace_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package benchmark 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "runtime/debug" 7 | "testing" 8 | 9 | "github.com/joomcode/errorx" 10 | ) 11 | 12 | var errorSink error 13 | 14 | func BenchmarkSimpleError10(b *testing.B) { 15 | for n := 0; n < b.N; n++ { 16 | errorSink = function0(10, createSimpleError) 17 | } 18 | consumeResult(errorSink) 19 | } 20 | 21 | func BenchmarkErrorxError10(b *testing.B) { 22 | for n := 0; n < b.N; n++ { 23 | errorSink = function0(10, createSimpleErrorxError) 24 | } 25 | consumeResult(errorSink) 26 | } 27 | 28 | func BenchmarkStackTraceErrorxError10(b *testing.B) { 29 | for n := 0; n < b.N; n++ { 30 | errorSink = function0(10, createErrorxError) 31 | } 32 | consumeResult(errorSink) 33 | } 34 | 35 | func BenchmarkSimpleError100(b *testing.B) { 36 | for n := 0; n < b.N; n++ { 37 | errorSink = function0(100, createSimpleError) 38 | } 39 | consumeResult(errorSink) 40 | } 41 | 42 | func BenchmarkErrorxError100(b *testing.B) { 43 | for n := 0; n < b.N; n++ { 44 | errorSink = function0(100, createSimpleErrorxError) 45 | } 46 | consumeResult(errorSink) 47 | } 48 | 49 | func BenchmarkStackTraceErrorxError100(b *testing.B) { 50 | for n := 0; n < b.N; n++ { 51 | errorSink = function0(100, createErrorxError) 52 | } 53 | consumeResult(errorSink) 54 | } 55 | 56 | func BenchmarkStackTraceNaiveError100(b *testing.B) { 57 | for n := 0; n < b.N; n++ { 58 | errorSink = function0(100, createNaiveError) 59 | } 60 | consumeResult(errorSink) 61 | } 62 | 63 | func BenchmarkSimpleErrorPrint100(b *testing.B) { 64 | for n := 0; n < b.N; n++ { 65 | err := function0(100, createSimpleError) 66 | emulateErrorPrint(err) 67 | errorSink = err 68 | } 69 | consumeResult(errorSink) 70 | } 71 | 72 | func BenchmarkErrorxErrorPrint100(b *testing.B) { 73 | for n := 0; n < b.N; n++ { 74 | err := function0(100, createSimpleErrorxError) 75 | emulateErrorPrint(err) 76 | errorSink = err 77 | } 78 | consumeResult(errorSink) 79 | } 80 | 81 | func BenchmarkStackTraceErrorxErrorPrint100(b *testing.B) { 82 | for n := 0; n < b.N; n++ { 83 | err := function0(100, createErrorxError) 84 | emulateErrorPrint(err) 85 | errorSink = err 86 | } 87 | consumeResult(errorSink) 88 | } 89 | 90 | func BenchmarkStackTraceNaiveErrorPrint100(b *testing.B) { 91 | for n := 0; n < b.N; n++ { 92 | err := function0(100, createNaiveError) 93 | emulateErrorPrint(err) 94 | errorSink = err 95 | } 96 | consumeResult(errorSink) 97 | } 98 | 99 | func createSimpleError() error { 100 | return errors.New("benchmark") 101 | } 102 | 103 | var ( 104 | Errors = errorx.NewNamespace("errorx.benchmark") 105 | NoStackTraceError = Errors.NewType("no_stack_trace").ApplyModifiers(errorx.TypeModifierOmitStackTrace) 106 | StackTraceError = Errors.NewType("stack_trace") 107 | ) 108 | 109 | func createSimpleErrorxError() error { 110 | return NoStackTraceError.New("benchmark") 111 | } 112 | 113 | func createErrorxError() error { 114 | return StackTraceError.New("benchmark") 115 | } 116 | 117 | type naiveError struct { 118 | stack []byte 119 | } 120 | 121 | func (err naiveError) Error() string { 122 | return fmt.Sprintf("benchmark\n%s", err.stack) 123 | } 124 | 125 | func createNaiveError() error { 126 | return naiveError{stack: debug.Stack()} 127 | } 128 | 129 | func function0(depth int, generate func() error) error { 130 | if depth == 0 { 131 | return generate() 132 | } 133 | 134 | switch depth % 3 { 135 | case 0: 136 | return function1(depth-1, generate) 137 | case 1: 138 | return function2(depth-1, generate) 139 | default: 140 | return function3(depth-1, generate) 141 | } 142 | } 143 | 144 | func function1(depth int, generate func() error) error { 145 | if depth == 0 { 146 | return generate() 147 | } 148 | 149 | return function4(depth-1, generate) 150 | } 151 | 152 | func function2(depth int, generate func() error) error { 153 | if depth == 0 { 154 | return generate() 155 | } 156 | 157 | return function4(depth-1, generate) 158 | } 159 | 160 | func function3(depth int, generate func() error) error { 161 | if depth == 0 { 162 | return generate() 163 | } 164 | 165 | return function4(depth-1, generate) 166 | } 167 | 168 | func function4(depth int, generate func() error) error { 169 | switch depth { 170 | case 0: 171 | return generate() 172 | default: 173 | return function0(depth-1, generate) 174 | } 175 | } 176 | 177 | type sinkError struct { 178 | value int 179 | } 180 | 181 | func (sinkError) Error() string { 182 | return "" 183 | } 184 | 185 | // Perform error formatting and consume the result to disallow optimizations against output 186 | func emulateErrorPrint(err error) { 187 | output := fmt.Sprintf("%+v", err) 188 | if len(output) > 10000 && output[1000:1004] == "DOOM" { 189 | panic("this was not supposed to happen") 190 | } 191 | } 192 | 193 | // Consume error with a possible side effect to disallow optimizations against err 194 | func consumeResult(err error) { 195 | if e, ok := err.(sinkError); ok && e.value == 1 { 196 | panic("this was not supposed to happen") 197 | } 198 | } 199 | 200 | // A public function to discourage optimizations against errorSink variable 201 | func ExportSink() error { 202 | return errorSink 203 | } 204 | -------------------------------------------------------------------------------- /builder.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | // ErrorBuilder is a utility to compose an error from type. 9 | // Typically, a direct usage is not required: either Type methods of helpers like Decorate are sufficient. 10 | // Only use builder if no simpler alternative is available. 11 | type ErrorBuilder struct { 12 | errorType *Type 13 | message string 14 | cause error 15 | mode callStackBuildMode 16 | isTransparent bool 17 | } 18 | 19 | // NewErrorBuilder creates error builder from an existing error type. 20 | func NewErrorBuilder(t *Type) ErrorBuilder { 21 | getMode := func() callStackBuildMode { 22 | if !t.modifiers.CollectStackTrace() { 23 | return stackTraceOmit 24 | } 25 | return stackTraceCollect 26 | } 27 | 28 | return ErrorBuilder{ 29 | errorType: t, 30 | mode: getMode(), 31 | isTransparent: t.modifiers.Transparent(), 32 | } 33 | } 34 | 35 | // WithCause provides an original cause for error. 36 | // For non-errorx errors, a stack trace is collected unless Type tells otherwise. 37 | // Otherwise, it is inherited by default, as error wrapping is typically performed 'en passe'. 38 | // Note that even if an original error explicitly omitted the stack trace, it could be added on wrap. 39 | func (eb ErrorBuilder) WithCause(err error) ErrorBuilder { 40 | eb.cause = err 41 | if Cast(err) != nil { 42 | if eb.errorType.modifiers.CollectStackTrace() { 43 | eb.mode = stackTraceBorrowOrCollect 44 | } else { 45 | eb.mode = stackTraceBorrowOnly 46 | } 47 | } 48 | 49 | return eb 50 | } 51 | 52 | // Transparent makes a wrap transparent rather than opaque (default). 53 | // Transparent wrap hides the current error type from the type checks and exposes the error type of the cause instead. 54 | // The same holds true for traits, and the dynamic properties are visible from both cause and transparent wrapper. 55 | // Note that if the cause error is non-errorx, transparency will still hold, type check against wrapper will still fail. 56 | func (eb ErrorBuilder) Transparent() ErrorBuilder { 57 | if eb.cause == nil { 58 | panic("wrong builder usage: wrap modifier without non-nil cause") 59 | } 60 | 61 | eb.isTransparent = true 62 | return eb 63 | } 64 | 65 | // EnhanceStackTrace is a signal to collect the current stack trace along with the original one, and use both in formatting. 66 | // If the original error does not hold a stack trace for whatever reason, it will be collected it this point. 67 | // This is typically a way to handle an error received from another goroutine - say, a worker pool. 68 | // When stack traces overlap, formatting makes a conservative attempt not to repeat itself, 69 | // preserving the *original* stack trace in its entirety. 70 | func (eb ErrorBuilder) EnhanceStackTrace() ErrorBuilder { 71 | if eb.cause == nil { 72 | panic("wrong builder usage: wrap modifier without non-nil cause") 73 | } 74 | 75 | if Cast(eb.cause) != nil { 76 | eb.mode = stackTraceEnhance 77 | } else { 78 | eb.mode = stackTraceCollect 79 | } 80 | 81 | return eb 82 | } 83 | 84 | // WithConditionallyFormattedMessage provides a message for an error in flexible format, to simplify its usages. 85 | // Without args, leaves the original message intact, so a message may be generated or provided externally. 86 | // With args, a formatting is performed, and it is therefore expected a format string to be constant. 87 | func (eb ErrorBuilder) WithConditionallyFormattedMessage(message string, args ...interface{}) ErrorBuilder { 88 | if len(args) == 0 { 89 | eb.message = message 90 | } else { 91 | eb.message = fmt.Sprintf(message, args...) 92 | } 93 | 94 | return eb 95 | } 96 | 97 | // Create returns an error with specified params. 98 | func (eb ErrorBuilder) Create() *Error { 99 | err := &Error{ 100 | errorType: eb.errorType, 101 | message: eb.message, 102 | cause: eb.cause, 103 | transparent: eb.isTransparent, 104 | stackTrace: eb.assembleStackTrace(), 105 | } 106 | return err 107 | } 108 | 109 | type callStackBuildMode int 110 | 111 | const ( 112 | stackTraceCollect callStackBuildMode = 1 113 | stackTraceBorrowOrCollect callStackBuildMode = 2 114 | stackTraceBorrowOnly callStackBuildMode = 3 115 | stackTraceEnhance callStackBuildMode = 4 116 | stackTraceOmit callStackBuildMode = 5 117 | ) 118 | 119 | func (eb ErrorBuilder) assembleStackTrace() *stackTrace { 120 | switch eb.mode { 121 | case stackTraceCollect: 122 | return eb.collectOriginalStackTrace() 123 | case stackTraceBorrowOnly: 124 | return eb.borrowStackTraceFromCause() 125 | case stackTraceBorrowOrCollect: 126 | if st := eb.borrowStackTraceFromCause(); st != nil { 127 | return st 128 | } 129 | 130 | return eb.collectOriginalStackTrace() 131 | case stackTraceEnhance: 132 | return eb.combineStackTraceWithCause() 133 | case stackTraceOmit: 134 | return nil 135 | default: 136 | panic("unknown mode " + strconv.Itoa(int(eb.mode))) 137 | } 138 | } 139 | 140 | func (eb ErrorBuilder) collectOriginalStackTrace() *stackTrace { 141 | return collectStackTrace() 142 | } 143 | 144 | func (eb ErrorBuilder) borrowStackTraceFromCause() *stackTrace { 145 | return eb.extractStackTraceFromCause(eb.cause) 146 | } 147 | 148 | func (eb ErrorBuilder) combineStackTraceWithCause() *stackTrace { 149 | currentStackTrace := collectStackTrace() 150 | 151 | originalStackTrace := eb.extractStackTraceFromCause(eb.cause) 152 | if originalStackTrace != nil { 153 | currentStackTrace.enhanceWithCause(originalStackTrace) 154 | } 155 | 156 | return currentStackTrace 157 | } 158 | 159 | func (eb ErrorBuilder) extractStackTraceFromCause(cause error) *stackTrace { 160 | if typedCause := Cast(cause); typedCause != nil { 161 | return typedCause.stackTrace 162 | } 163 | 164 | return nil 165 | } 166 | -------------------------------------------------------------------------------- /builder_test.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestBuilderTransparency(t *testing.T) { 12 | t.Run("Raw", func(t *testing.T) { 13 | err := NewErrorBuilder(testType).WithCause(errors.New("bad thing")).Transparent().Create() 14 | require.False(t, err.IsOfType(testType)) 15 | require.NotEqual(t, testType, err.Type()) 16 | }) 17 | 18 | t.Run("RawWithModifier", func(t *testing.T) { 19 | err := NewErrorBuilder(testTypeTransparent).WithCause(errors.New("bad thing")).Create() 20 | require.False(t, err.IsOfType(testType)) 21 | require.NotEqual(t, testType, err.Type()) 22 | }) 23 | } 24 | 25 | func testBuilderRespectsNoStackTraceMarkerFrame() error { 26 | return testType.NewWithNoMessage() 27 | } 28 | 29 | func TestBuilderRespectsNoStackTrace(t *testing.T) { 30 | wrapperErrorTypes := []*Type{testTypeSilent, testTypeSilentTransparent} 31 | 32 | for _, et := range wrapperErrorTypes { 33 | t.Run(et.String(), func(t *testing.T) { 34 | t.Run("Naked", func(t *testing.T) { 35 | err := NewErrorBuilder(et). 36 | WithCause(errors.New("naked error")). 37 | Create() 38 | require.Nil(t, err.stackTrace) 39 | }) 40 | 41 | t.Run("WithoutStacktrace", func(t *testing.T) { 42 | err := NewErrorBuilder(et). 43 | WithCause(testTypeSilent.NewWithNoMessage()). 44 | Create() 45 | require.Nil(t, err.stackTrace) 46 | }) 47 | 48 | t.Run("WithStacktrace", func(t *testing.T) { 49 | cause := testBuilderRespectsNoStackTraceMarkerFrame() 50 | err := NewErrorBuilder(et). 51 | WithCause(cause). 52 | Create() 53 | require.Same(t, err.stackTrace, Cast(cause).stackTrace) 54 | require.Contains(t, fmt.Sprintf("%+v", err), "testBuilderRespectsNoStackTraceMarkerFrame") 55 | }) 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | var ( 4 | // CommonErrors is a namespace for general purpose errors designed for universal use. 5 | // These errors should typically be used in opaque manner, implying no handing in user code. 6 | // When handling is required, it is best to use custom error types with both standard and custom traits. 7 | CommonErrors = NewNamespace("common") 8 | 9 | // IllegalArgument is a type for invalid argument error 10 | IllegalArgument = CommonErrors.NewType("illegal_argument") 11 | // IllegalState is a type for invalid state error 12 | IllegalState = CommonErrors.NewType("illegal_state") 13 | // IllegalFormat is a type for invalid format error 14 | IllegalFormat = CommonErrors.NewType("illegal_format") 15 | // InitializationFailed is a type for initialization error 16 | InitializationFailed = CommonErrors.NewType("initialization_failed") 17 | // DataUnavailable is a type for unavailable data error 18 | DataUnavailable = CommonErrors.NewType("data_unavailable") 19 | // UnsupportedOperation is a type for unsupported operation error 20 | UnsupportedOperation = CommonErrors.NewType("unsupported_operation") 21 | // RejectedOperation is a type for rejected operation error 22 | RejectedOperation = CommonErrors.NewType("rejected_operation") 23 | // Interrupted is a type for interruption error 24 | Interrupted = CommonErrors.NewType("interrupted") 25 | // AssertionFailed is a type for assertion error 26 | AssertionFailed = CommonErrors.NewType("assertion_failed") 27 | // InternalError is a type for internal error 28 | InternalError = CommonErrors.NewType("internal_error") 29 | // ExternalError is a type for external error 30 | ExternalError = CommonErrors.NewType("external_error") 31 | // ConcurrentUpdate is a type for concurrent update error 32 | ConcurrentUpdate = CommonErrors.NewType("concurrent_update") 33 | // TimeoutElapsed is a type for timeout error 34 | TimeoutElapsed = CommonErrors.NewType("timeout", Timeout()) 35 | // NotImplemented is an error type for lacking implementation 36 | NotImplemented = UnsupportedOperation.NewSubtype("not_implemented") 37 | // UnsupportedVersion is a type for unsupported version error 38 | UnsupportedVersion = UnsupportedOperation.NewSubtype("version") 39 | ) 40 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | // Error is an instance of error object. 10 | // At the moment of creation, Error collects information based on context, creation modifiers and type it belongs to. 11 | // Error is mostly immutable, and distinct errors composition is achieved through wrap. 12 | type Error struct { 13 | message string 14 | errorType *Type 15 | cause error 16 | stackTrace *stackTrace 17 | // properties are used both for public properties inherited through "transparent" wrapping 18 | // and for some optional per-instance information like "underlying errors" 19 | properties *propertyMap 20 | 21 | transparent bool 22 | hasUnderlying bool 23 | printablePropertyCount uint8 24 | } 25 | 26 | var _ fmt.Formatter = (*Error)(nil) 27 | 28 | // WithProperty adds a dynamic property to error instance. 29 | // If an error already contained another value for the same property, it is overwritten. 30 | // It is a caller's responsibility to accumulate and update a property, if needed. 31 | // Dynamic properties is a brittle mechanism and should therefore be used with care and in a simple and robust manner. 32 | // Currently, properties are implemented as a linked list, therefore it is not safe to have many dozens of them. But couple of dozen is just ok. 33 | func (e *Error) WithProperty(key Property, value interface{}) *Error { 34 | errorCopy := *e 35 | errorCopy.properties = errorCopy.properties.with(key, value) 36 | if key.printable && errorCopy.printablePropertyCount < 255 { 37 | errorCopy.printablePropertyCount++ 38 | } 39 | return &errorCopy 40 | } 41 | 42 | // WithUnderlyingErrors adds multiple additional related (hidden, suppressed) errors to be used exclusively in error output. 43 | // Note that these errors make no other effect whatsoever: their traits, types, properties etc. are lost on the observer. 44 | // Consider using errorx.DecorateMany instead. 45 | func (e *Error) WithUnderlyingErrors(errs ...error) *Error { 46 | underlying := e.underlying() 47 | newUnderlying := underlying 48 | 49 | for _, err := range errs { 50 | if err == nil { 51 | continue 52 | } 53 | 54 | newUnderlying = append(newUnderlying, err) 55 | } 56 | 57 | if len(newUnderlying) == len(underlying) { 58 | return e 59 | } 60 | 61 | l := len(newUnderlying) // note: l > 0, because non-increased 0 length is handled above 62 | errorCopy := e.WithProperty(propertyUnderlying, newUnderlying[:l:l]) 63 | errorCopy.hasUnderlying = true 64 | return errorCopy 65 | } 66 | 67 | // Property extracts a dynamic property value from an error. 68 | // A property may belong to this error or be extracted from the original cause. 69 | // The transparency rules are respected to some extent: both the original cause and the transparent wrapper 70 | // may have accessible properties, but an opaque wrapper hides the original properties. 71 | func (e *Error) Property(key Property) (interface{}, bool) { 72 | cause := e 73 | for cause != nil { 74 | value, ok := cause.properties.get(key) 75 | if ok { 76 | return value, true 77 | } 78 | 79 | if !cause.transparent { 80 | break 81 | } 82 | 83 | cause = Cast(cause.Cause()) 84 | } 85 | 86 | return nil, false 87 | } 88 | 89 | // HasTrait checks if an error possesses the expected trait. 90 | // Trait check works just as a type check would: opaque wrap hides the traits of the cause. 91 | // Traits are always properties of a type rather than of an instance, so trait check is an alternative to a type check. 92 | // This alternative is preferable, though, as it is less brittle and generally creates less of a dependency. 93 | func (e *Error) HasTrait(key Trait) bool { 94 | cause := e 95 | for cause != nil { 96 | if !cause.transparent { 97 | return cause.errorType.HasTrait(key) 98 | } 99 | 100 | cause = Cast(cause.Cause()) 101 | } 102 | 103 | return false 104 | } 105 | 106 | // IsOfType is a proper type check for an errorx-based errors. 107 | // It takes the transparency and error types hierarchy into account, 108 | // so that type check against any supertype of the original cause passes. 109 | // Go 1.13 and above: it also tolerates non-errorx errors in chain if those errors support errors unwrap. 110 | func (e *Error) IsOfType(t *Type) bool { 111 | return e.isOfType(t) 112 | } 113 | 114 | // Type returns the exact type of this error. 115 | // With transparent wrapping, such as in Decorate(), returns the type of the original cause. 116 | // The result is always not nil, even if the resulting type is impossible to successfully type check against. 117 | // 118 | // NB: the exact error type may fail an equality check where a IsOfType() check would succeed. 119 | // This may happen if a type is checked against one of its supertypes, for example. 120 | // Therefore, handle direct type checks with care or avoid it altogether and use TypeSwitch() or IsForType() instead. 121 | func (e *Error) Type() *Type { 122 | cause := e 123 | for cause != nil { 124 | if !cause.transparent { 125 | return cause.errorType 126 | } 127 | 128 | cause = Cast(cause.Cause()) 129 | } 130 | 131 | return foreignType 132 | } 133 | 134 | // Message returns a message of this particular error, disregarding the cause. 135 | // The result of this method, like a result of an Error() method, should never be used to infer the meaning of an error. 136 | // In most cases, message is only used as a part of formatting to print error contents into a log file. 137 | // Manual extraction may be required, however, to transform an error into another format - say, API response. 138 | func (e *Error) Message() string { 139 | return e.message 140 | } 141 | 142 | // Cause returns the immediate (wrapped) cause of current error. 143 | // This method could be used to dig for root cause of the error, but it is not advised to do so. 144 | // Errors should not require a complex navigation through causes to be properly handled, and the need to do so is a code smell. 145 | // Manually extracting cause defeats features such as opaque wrap, behaviour of properties etc. 146 | // This method is, therefore, reserved for system utilities, not for general use. 147 | func (e *Error) Cause() error { 148 | return e.cause 149 | } 150 | 151 | // Is returns true if and only if target is errorx error that passes errorx type check against current error. 152 | // This behaviour is exactly the same as that of IsOfType(). 153 | // See also: errors.Is() 154 | func (e *Error) Is(target error) bool { 155 | typedTarget := Cast(target) 156 | return typedTarget != nil && IsOfType(e, typedTarget.Type()) 157 | } 158 | 159 | // From errors package: if e.Unwrap() returns a non-nil error w, then we say that e wraps w. 160 | // Unwrap returns cause of current error in case it is wrapped transparently, nil otherwise. 161 | // See also: errors.Unwrap() 162 | func (e *Error) Unwrap() error { 163 | if e != nil && e.cause != nil && e.transparent { 164 | return e.cause 165 | } else { 166 | return nil 167 | } 168 | } 169 | 170 | // Format implements the Formatter interface. 171 | // Supported verbs: 172 | // 173 | // %s simple message output 174 | // %v same as %s 175 | // %+v full output complete with a stack trace 176 | // 177 | // In is nearly always preferable to use %+v format. 178 | // If a stack trace is not required, it should be omitted at the moment of creation rather in formatting. 179 | func (e *Error) Format(s fmt.State, verb rune) { 180 | message := e.fullMessage() 181 | switch verb { 182 | case 'v': 183 | _, _ = io.WriteString(s, message) 184 | if s.Flag('+') { 185 | e.stackTrace.Format(s, verb) 186 | } 187 | case 's': 188 | _, _ = io.WriteString(s, message) 189 | } 190 | } 191 | 192 | // Error implements the error interface. 193 | // A result is the same as with %s formatter and does not contain a stack trace. 194 | func (e *Error) Error() string { 195 | return e.fullMessage() 196 | } 197 | 198 | func (e *Error) fullMessage() string { 199 | if e.transparent { 200 | return e.messageWithUnderlyingInfo() 201 | } 202 | return joinStringsIfNonEmpty(": ", e.errorType.FullName(), e.messageWithUnderlyingInfo()) 203 | } 204 | 205 | func (e *Error) messageWithUnderlyingInfo() string { 206 | return joinStringsIfNonEmpty(" ", e.messageText(), e.underlyingInfo()) 207 | } 208 | 209 | func (e *Error) underlyingInfo() string { 210 | if !e.hasUnderlying { 211 | return "" 212 | } 213 | 214 | underlying := e.underlying() 215 | infos := make([]string, 0, len(underlying)) 216 | for _, err := range underlying { 217 | infos = append(infos, err.Error()) 218 | } 219 | 220 | return fmt.Sprintf("(hidden: %s)", joinStringsIfNonEmpty(", ", infos...)) 221 | } 222 | 223 | func (e *Error) messageFromProperties() string { 224 | if e.printablePropertyCount == 0 { 225 | return "" 226 | } 227 | uniq := make(map[Property]struct{}, e.printablePropertyCount) 228 | strs := make([]string, 0, e.printablePropertyCount) 229 | for m := e.properties; m != nil; m = m.next { 230 | if !m.p.printable { 231 | continue 232 | } 233 | if _, ok := uniq[m.p]; ok { 234 | continue 235 | } 236 | uniq[m.p] = struct{}{} 237 | strs = append(strs, fmt.Sprintf("%s: %v", m.p.label, m.value)) 238 | } 239 | return "{" + strings.Join(strs, ", ") + "}" 240 | } 241 | 242 | func (e *Error) underlying() []error { 243 | if !e.hasUnderlying { 244 | return nil 245 | } 246 | // Note: properties are used as storage for optional "underlying errors". 247 | // Chain of cause should not be traversed here. 248 | u, _ := e.properties.get(propertyUnderlying) 249 | return u.([]error) 250 | } 251 | 252 | func (e *Error) messageText() string { 253 | message := joinStringsIfNonEmpty(" ", e.message, e.messageFromProperties()) 254 | if cause := e.Cause(); cause != nil { 255 | return joinStringsIfNonEmpty(", cause: ", message, cause.Error()) 256 | } 257 | return message 258 | } 259 | -------------------------------------------------------------------------------- /error_112.go: -------------------------------------------------------------------------------- 1 | // +build !go1.13 2 | 3 | package errorx 4 | 5 | func isOfType(err error, t *Type) bool { 6 | e := Cast(err) 7 | return e != nil && e.IsOfType(t) 8 | } 9 | 10 | func (e *Error) isOfType(t *Type) bool { 11 | cause := e 12 | for cause != nil { 13 | if !cause.transparent { 14 | return cause.errorType.IsOfType(t) 15 | } 16 | 17 | cause = Cast(cause.Cause()) 18 | } 19 | 20 | return false 21 | } 22 | 23 | 24 | -------------------------------------------------------------------------------- /error_113.go: -------------------------------------------------------------------------------- 1 | // +build go1.13 2 | 3 | package errorx 4 | 5 | import "errors" 6 | 7 | func isOfType(err error, t *Type) bool { 8 | e := burrowForTyped(err) 9 | return e != nil && e.IsOfType(t) 10 | } 11 | 12 | func (e *Error) isOfType(t *Type) bool { 13 | cause := e 14 | for cause != nil { 15 | if !cause.transparent { 16 | return cause.errorType.IsOfType(t) 17 | } 18 | 19 | cause = burrowForTyped(cause.Cause()) 20 | } 21 | 22 | return false 23 | } 24 | 25 | // burrowForTyped returns either the first *Error in unwrap chain or nil 26 | func burrowForTyped(err error) *Error { 27 | raw := err 28 | for raw != nil { 29 | typed := Cast(raw) 30 | if typed != nil { 31 | return typed 32 | } 33 | 34 | raw = errors.Unwrap(raw) 35 | } 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /error_113_test.go: -------------------------------------------------------------------------------- 1 | // +build go1.13 2 | 3 | package errorx 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "io" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestErrorUnwrap(t *testing.T) { 15 | t.Run("Trivial", func(t *testing.T) { 16 | err := testType.NewWithNoMessage() 17 | unwrapped := errors.Unwrap(err) 18 | require.Nil(t, unwrapped) 19 | }) 20 | 21 | t.Run("Wrap", func(t *testing.T) { 22 | err := testTypeBar1.Wrap(testType.NewWithNoMessage(), "") 23 | unwrapped := errors.Unwrap(err) 24 | require.Nil(t, unwrapped) 25 | }) 26 | 27 | t.Run("WrapForeign", func(t *testing.T) { 28 | err := testTypeBar1.Wrap(io.EOF, "") 29 | unwrapped := errors.Unwrap(err) 30 | require.Nil(t, unwrapped) 31 | }) 32 | 33 | t.Run("Decorate", func(t *testing.T) { 34 | err := Decorate(testType.NewWithNoMessage(), "") 35 | unwrapped := errors.Unwrap(err) 36 | require.NotNil(t, unwrapped) 37 | require.True(t, IsOfType(unwrapped, testType)) 38 | require.True(t, Cast(unwrapped).Type() == testType) 39 | }) 40 | 41 | t.Run("DecorateForeign", func(t *testing.T) { 42 | err := Decorate(io.EOF, "") 43 | unwrapped := errors.Unwrap(err) 44 | require.NotNil(t, unwrapped) 45 | require.True(t, errors.Is(unwrapped, io.EOF)) 46 | require.True(t, unwrapped == io.EOF) 47 | }) 48 | 49 | t.Run("Nested", func(t *testing.T) { 50 | err := Decorate(Decorate(testType.NewWithNoMessage(), ""), "") 51 | unwrapped := errors.Unwrap(err) 52 | require.NotNil(t, unwrapped) 53 | unwrapped = errors.Unwrap(unwrapped) 54 | require.NotNil(t, unwrapped) 55 | require.True(t, IsOfType(unwrapped, testType)) 56 | }) 57 | 58 | t.Run("NestedWrapped", func(t *testing.T) { 59 | err := Decorate(testTypeBar1.Wrap(testType.NewWithNoMessage(), ""), "") 60 | unwrapped := errors.Unwrap(err) 61 | require.NotNil(t, unwrapped) 62 | require.True(t, IsOfType(unwrapped, testTypeBar1)) 63 | unwrapped = errors.Unwrap(unwrapped) 64 | require.Nil(t, unwrapped) 65 | }) 66 | 67 | t.Run("NestedForeign", func(t *testing.T) { 68 | err := Decorate(Decorate(io.EOF, ""), "") 69 | unwrapped := errors.Unwrap(err) 70 | require.NotNil(t, unwrapped) 71 | unwrapped = errors.Unwrap(unwrapped) 72 | require.NotNil(t, unwrapped) 73 | require.True(t, errors.Is(unwrapped, io.EOF)) 74 | }) 75 | } 76 | 77 | func TestErrorIs(t *testing.T) { 78 | t.Run("Trivial", func(t *testing.T) { 79 | err := testType.NewWithNoMessage() 80 | require.True(t, errors.Is(err, testType.NewWithNoMessage())) 81 | require.False(t, errors.Is(err, testTypeBar1.NewWithNoMessage())) 82 | }) 83 | 84 | t.Run("Wrap", func(t *testing.T) { 85 | err := testTypeBar1.Wrap(testType.NewWithNoMessage(), "") 86 | require.False(t, errors.Is(err, testType.NewWithNoMessage())) 87 | require.True(t, errors.Is(err, testTypeBar1.NewWithNoMessage())) 88 | }) 89 | 90 | t.Run("Supertype", func(t *testing.T) { 91 | err := testSubtype0.Wrap(testTypeBar1.NewWithNoMessage(), "") 92 | require.True(t, errors.Is(err, testType.NewWithNoMessage())) 93 | require.True(t, errors.Is(err, testSubtype0.NewWithNoMessage())) 94 | require.False(t, errors.Is(err, testTypeBar1.NewWithNoMessage())) 95 | }) 96 | 97 | t.Run("Decorate", func(t *testing.T) { 98 | err := Decorate(testType.NewWithNoMessage(), "") 99 | require.True(t, errors.Is(err, testType.NewWithNoMessage())) 100 | }) 101 | 102 | t.Run("DecorateForeign", func(t *testing.T) { 103 | err := Decorate(io.EOF, "") 104 | require.True(t, errors.Is(err, io.EOF)) 105 | }) 106 | } 107 | 108 | func TestErrorsAndErrorx(t *testing.T) { 109 | t.Run("DecoratedForeign", func(t *testing.T) { 110 | err := fmt.Errorf("error test: %w", testType.NewWithNoMessage()) 111 | require.True(t, errors.Is(err, testType.NewWithNoMessage())) 112 | require.True(t, IsOfType(err, testType)) 113 | }) 114 | 115 | t.Run("LayeredDecorate", func(t *testing.T) { 116 | err := Decorate(fmt.Errorf("error test: %w", testType.NewWithNoMessage()), "test") 117 | require.True(t, errors.Is(err, testType.NewWithNoMessage())) 118 | require.True(t, IsOfType(err, testType)) 119 | }) 120 | 121 | t.Run("LayeredDecorateAgain", func(t *testing.T) { 122 | err := fmt.Errorf("error test: %w", Decorate(io.EOF, "test")) 123 | require.True(t, errors.Is(err, io.EOF)) 124 | }) 125 | 126 | t.Run("Wrap", func(t *testing.T) { 127 | err := fmt.Errorf("error test: %w", testType.Wrap(io.EOF, "test")) 128 | require.False(t, errors.Is(err, io.EOF)) 129 | require.True(t, errors.Is(err, testType.NewWithNoMessage())) 130 | }) 131 | } -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | var ( 12 | testNamespace = NewNamespace("foo") 13 | testType = testNamespace.NewType("bar") 14 | testTypeSilent = testType.NewSubtype("silent").ApplyModifiers(TypeModifierOmitStackTrace) 15 | testTypeTransparent = testType.NewSubtype("transparent").ApplyModifiers(TypeModifierTransparent) 16 | testTypeSilentTransparent = testType.NewSubtype("silent_transparent").ApplyModifiers(TypeModifierTransparent, TypeModifierOmitStackTrace) 17 | testSubtype0 = testType.NewSubtype("internal") 18 | testSubtype1 = testSubtype0.NewSubtype("wat") 19 | testTypeBar1 = testNamespace.NewType("bar1") 20 | testTypeBar2 = testNamespace.NewType("bar2") 21 | ) 22 | 23 | func TestError(t *testing.T) { 24 | err := testType.NewWithNoMessage() 25 | require.Equal(t, "foo.bar", err.Error()) 26 | } 27 | 28 | func TestErrorWithMessage(t *testing.T) { 29 | err := testType.New("oops") 30 | require.Equal(t, "foo.bar: oops", err.Error()) 31 | } 32 | 33 | func TestErrorMessageWithCause(t *testing.T) { 34 | err := testSubtype1.WrapWithNoMessage(testType.New("fatal")) 35 | require.Equal(t, "foo.bar.internal.wat: foo.bar: fatal", err.Error()) 36 | } 37 | 38 | func TestErrorWrap(t *testing.T) { 39 | err0 := testType.NewWithNoMessage() 40 | err1 := testTypeBar1.Wrap(err0, "a") 41 | 42 | require.Nil(t, Ignore(err1, testTypeBar1)) 43 | require.NotNil(t, Ignore(err1, testType)) 44 | } 45 | 46 | func TestErrorDecorate(t *testing.T) { 47 | err0 := testType.NewWithNoMessage() 48 | err1 := testTypeBar1.Wrap(err0, "a") 49 | err2 := Decorate(err1, "b") 50 | 51 | require.NotNil(t, Ignore(err2, testTypeBar2)) 52 | require.Nil(t, Ignore(err2, testTypeBar1)) 53 | require.NotNil(t, Ignore(err2, testType)) 54 | } 55 | 56 | func TestErrorMessages(t *testing.T) { 57 | t.Run("Subtypes", func(t *testing.T) { 58 | require.Equal(t, "foo.bar.internal.wat", testSubtype1.NewWithNoMessage().Error()) 59 | require.Equal(t, "foo.bar.internal.wat: oops", testSubtype1.New("oops").Error()) 60 | }) 61 | 62 | t.Run("Wrapped", func(t *testing.T) { 63 | cause := testType.New("poof!") 64 | require.Equal(t, "foo.bar.internal.wat: foo.bar: poof!", testSubtype1.Wrap(cause, "").Error()) 65 | require.Equal(t, "foo.bar.internal.wat: foo.bar: poof!", testSubtype1.WrapWithNoMessage(cause).Error()) 66 | require.Equal(t, "foo.bar.internal.wat: oops, cause: foo.bar: poof!", testSubtype1.Wrap(cause, "oops").Error()) 67 | }) 68 | 69 | t.Run("Complex", func(t *testing.T) { 70 | innerCause := NewNamespace("c").NewType("d").Wrap(errors.New("Achtung!"), "panic") 71 | stackedError := testSubtype1.Wrap(testType.Wrap(innerCause, "poof!"), "") 72 | require.Equal(t, "foo.bar.internal.wat: foo.bar: poof!, cause: c.d: panic, cause: Achtung!", stackedError.Error()) 73 | }) 74 | } 75 | 76 | func TestImmutableError(t *testing.T) { 77 | t.Run("Property", func(t *testing.T) { 78 | err := testType.NewWithNoMessage() 79 | err1 := err.WithProperty(PropertyPayload(), 1) 80 | err2 := err1.WithProperty(PropertyPayload(), 2) 81 | 82 | require.True(t, err.errorType.IsOfType(err2.errorType)) 83 | require.Equal(t, err.message, err2.message) 84 | 85 | payload, ok := ExtractPayload(err) 86 | require.False(t, ok) 87 | 88 | payload, ok = ExtractPayload(err1) 89 | require.True(t, ok) 90 | require.EqualValues(t, 1, payload) 91 | 92 | payload, ok = ExtractPayload(err2) 93 | require.True(t, ok) 94 | require.EqualValues(t, 2, payload) 95 | }) 96 | 97 | t.Run("Underlying", func(t *testing.T) { 98 | err := testType.NewWithNoMessage() 99 | err1 := err.WithUnderlyingErrors(testSubtype0.NewWithNoMessage()) 100 | err2 := err1.WithUnderlyingErrors(testSubtype1.NewWithNoMessage()) 101 | 102 | require.True(t, err.errorType.IsOfType(err2.errorType)) 103 | require.Equal(t, err.message, err2.message) 104 | 105 | require.Len(t, err.underlying(), 0) 106 | require.Len(t, err1.underlying(), 1) 107 | require.Len(t, err2.underlying(), 2) 108 | }) 109 | } 110 | 111 | func TestErrorStackTrace(t *testing.T) { 112 | err := createErrorFuncInStackTrace(testType) 113 | output := fmt.Sprintf("%+v", err) 114 | require.Contains(t, output, "createErrorFuncInStackTrace", output) 115 | require.Contains(t, output, "TestErrorStackTrace", output) 116 | } 117 | 118 | func TestEnhancedStackTrace(t *testing.T) { 119 | err := createWrappedErrorFuncOuterInStackTrace(testType) 120 | output := fmt.Sprintf("%+v", err) 121 | require.Contains(t, output, "createWrappedErrorFuncOuterInStackTrace", output) 122 | require.Contains(t, output, "createErrorInAnotherGoroutine", output) 123 | } 124 | 125 | func TestDecorate(t *testing.T) { 126 | err := Decorate(testType.NewWithNoMessage(), "ouch!") 127 | require.Equal(t, "ouch!, cause: foo.bar", err.Error()) 128 | require.True(t, IsOfType(err, testType)) 129 | require.Equal(t, testType, err.Type()) 130 | } 131 | 132 | func TestUnderlyingInFormat(t *testing.T) { 133 | err := DecorateMany("this is terribly bad", testTypeBar1.Wrap(testSubtype1.NewWithNoMessage(), "real bad"), testTypeBar2.New("bad")) 134 | require.Equal(t, "synthetic.wrap: this is terribly bad, cause: foo.bar1: real bad, cause: foo.bar.internal.wat (hidden: foo.bar2: bad)", err.Error()) 135 | 136 | err = DecorateMany("this is terribly bad", testTypeBar1.New("real bad"), testTypeBar2.Wrap(testSubtype1.NewWithNoMessage(), "bad")) 137 | require.Equal(t, "synthetic.wrap: this is terribly bad, cause: foo.bar1: real bad (hidden: foo.bar2: bad, cause: foo.bar.internal.wat)", err.Error()) 138 | } 139 | 140 | func createErrorFuncInStackTrace(et *Type) *Error { 141 | err := et.NewWithNoMessage() 142 | return err 143 | } 144 | 145 | func createWrappedErrorFuncOuterInStackTrace(et *Type) *Error { 146 | return createWrappedErrorFuncInnerInStackTrace(et) 147 | } 148 | 149 | func createWrappedErrorFuncInnerInStackTrace(et *Type) *Error { 150 | channel := make(chan *Error) 151 | go func() { 152 | createErrorInAnotherGoroutine(et, channel) 153 | }() 154 | 155 | errFromChan := <-channel 156 | return EnhanceStackTrace(errFromChan, "wrap") 157 | } 158 | 159 | func createErrorInAnotherGoroutine(et *Type, channel chan *Error) { 160 | channel <- et.NewWithNoMessage() 161 | } 162 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package errorx_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/joomcode/errorx" 6 | ) 7 | 8 | func ExampleDecorate() { 9 | err := someFunc() 10 | fmt.Println(err.Error()) 11 | 12 | err = errorx.Decorate(err, "decorate") 13 | fmt.Println(err.Error()) 14 | 15 | err = errorx.Decorate(err, "outer decorate") 16 | fmt.Println(err.Error()) 17 | 18 | // Output: common.assertion_failed: example 19 | // decorate, cause: common.assertion_failed: example 20 | // outer decorate, cause: decorate, cause: common.assertion_failed: example 21 | } 22 | 23 | func ExampleDecorateMany() { 24 | err0 := someFunc() 25 | err1 := someFunc() 26 | err := errorx.DecorateMany("both calls failed", err0, err1) 27 | fmt.Println(err.Error()) 28 | 29 | // Output: both calls failed, cause: common.assertion_failed: example (hidden: common.assertion_failed: example) 30 | } 31 | 32 | func ExampleError_WithUnderlyingErrors() { 33 | fn := func() error { 34 | bytes, err := getBodyAndError() 35 | if err != nil { 36 | _, unmarshalErr := getDetailsFromBody(bytes) 37 | if unmarshalErr != nil { 38 | return errorx.AssertionFailed.Wrap(err, "failed to read details").WithUnderlyingErrors(unmarshalErr) 39 | } 40 | } 41 | 42 | return nil 43 | } 44 | 45 | fmt.Println(fn().Error()) 46 | // Output: common.assertion_failed: failed to read details, cause: common.assertion_failed: example (hidden: common.illegal_format) 47 | } 48 | 49 | func ExampleType_Wrap() { 50 | originalErr := errorx.IllegalArgument.NewWithNoMessage() 51 | err := errorx.AssertionFailed.Wrap(originalErr, "wrapped") 52 | 53 | fmt.Println(errorx.IsOfType(originalErr, errorx.IllegalArgument)) 54 | fmt.Println(errorx.IsOfType(err, errorx.IllegalArgument)) 55 | fmt.Println(errorx.IsOfType(err, errorx.AssertionFailed)) 56 | fmt.Println(err.Error()) 57 | 58 | // Output: 59 | // true 60 | // false 61 | // true 62 | // common.assertion_failed: wrapped, cause: common.illegal_argument 63 | } 64 | 65 | func ExampleError_Format() { 66 | err := nestedCall() 67 | 68 | simpleOutput := fmt.Sprintf("Error short: %v\n", err) 69 | verboseOutput := fmt.Sprintf("Error full: %+v", err) 70 | 71 | fmt.Println(simpleOutput) 72 | fmt.Println(verboseOutput) 73 | 74 | // Example output: 75 | //Error short: common.assertion_failed: example 76 | // 77 | //Error full: common.assertion_failed: example 78 | // at github.com/joomcode/errorx_test.someFunc() 79 | // /Users/username/go/src/github.com/joomcode/errorx/example_test.go:102 80 | // at github.com/joomcode/errorx_test.nestedCall() 81 | // /Users/username/go/src/github.com/joomcode/errorx/example_test.go:98 82 | // at github.com/joomcode/errorx_test.ExampleError_Format() 83 | // /Users/username/go/src/github.com/joomcode/errorx/example_test.go:66 84 | // <...> more 85 | } 86 | 87 | func ExampleEnhanceStackTrace() { 88 | errCh := make(chan error) 89 | go func() { 90 | errCh <- nestedCall() 91 | }() 92 | 93 | err := <-errCh 94 | verboseOutput := fmt.Sprintf("Error full: %+v", errorx.EnhanceStackTrace(err, "another goroutine")) 95 | fmt.Println(verboseOutput) 96 | 97 | // Example output: 98 | //Error full: another goroutine, cause: common.assertion_failed: example 99 | // at github.com/joomcode/errorx_test.ExampleEnhanceStackTrace() 100 | // /Users/username/go/src/github.com/joomcode/errorx/example_test.go:94 101 | // at testing.runExample() 102 | // /usr/local/Cellar/go/1.10.3/libexec/src/testing/example.go:122 103 | // at testing.runExamples() 104 | // /usr/local/Cellar/go/1.10.3/libexec/src/testing/example.go:46 105 | // at testing.(*M).Run() 106 | // /usr/local/Cellar/go/1.10.3/libexec/src/testing/testing.go:979 107 | // at main.main() 108 | // _testmain.go:146 109 | // ... 110 | // (1 duplicated frames) 111 | // ---------------------------------- 112 | // at github.com/joomcode/errorx_test.someFunc() 113 | // /Users/username/go/src/github.com/joomcode/errorx/example_test.go:106 114 | // at github.com/joomcode/errorx_test.nestedCall() 115 | // /Users/username/go/src/github.com/joomcode/errorx/example_test.go:102 116 | // at github.com/joomcode/errorx_test.ExampleEnhanceStackTrace.func1() 117 | // /Users/username/go/src/github.com/joomcode/errorx/example_test.go:90 118 | // at runtime.goexit() 119 | // /usr/local/Cellar/go/1.10.3/libexec/src/runtime/asm_amd64.s:2361 120 | } 121 | 122 | func ExampleIgnore() { 123 | err := errorx.IllegalArgument.NewWithNoMessage() 124 | err = errorx.Decorate(err, "more info") 125 | 126 | fmt.Println(err) 127 | fmt.Println(errorx.Ignore(err, errorx.IllegalArgument)) 128 | fmt.Println(errorx.Ignore(err, errorx.AssertionFailed)) 129 | 130 | // Output: 131 | // more info, cause: common.illegal_argument 132 | // 133 | // more info, cause: common.illegal_argument 134 | } 135 | 136 | func ExampleIgnoreWithTrait() { 137 | err := errorx.TimeoutElapsed.NewWithNoMessage() 138 | err = errorx.Decorate(err, "more info") 139 | 140 | fmt.Println(err) 141 | fmt.Println(errorx.IgnoreWithTrait(err, errorx.Timeout())) 142 | fmt.Println(errorx.IgnoreWithTrait(err, errorx.NotFound())) 143 | 144 | // Output: 145 | // more info, cause: common.timeout 146 | // 147 | // more info, cause: common.timeout 148 | } 149 | 150 | func ExampleIsOfType() { 151 | err0 := errorx.DataUnavailable.NewWithNoMessage() 152 | err1 := errorx.Decorate(err0, "decorated") 153 | err2 := errorx.RejectedOperation.Wrap(err0, "wrapped") 154 | 155 | fmt.Println(errorx.IsOfType(err0, errorx.DataUnavailable)) 156 | fmt.Println(errorx.IsOfType(err1, errorx.DataUnavailable)) 157 | fmt.Println(errorx.IsOfType(err2, errorx.DataUnavailable)) 158 | 159 | // Output: 160 | // true 161 | // true 162 | // false 163 | } 164 | 165 | func ExampleTypeSwitch() { 166 | err := errorx.DataUnavailable.NewWithNoMessage() 167 | 168 | switch errorx.TypeSwitch(err, errorx.DataUnavailable) { 169 | case errorx.DataUnavailable: 170 | fmt.Println("good") 171 | case nil: 172 | fmt.Println("bad") 173 | default: 174 | fmt.Println("bad") 175 | } 176 | 177 | switch errorx.TypeSwitch(nil, errorx.DataUnavailable) { 178 | case errorx.DataUnavailable: 179 | fmt.Println("bad") 180 | case nil: 181 | fmt.Println("good") 182 | default: 183 | fmt.Println("bad") 184 | } 185 | 186 | switch errorx.TypeSwitch(err, errorx.TimeoutElapsed) { 187 | case errorx.TimeoutElapsed: 188 | fmt.Println("bad") 189 | case nil: 190 | fmt.Println("bad") 191 | default: 192 | fmt.Println("good") 193 | } 194 | 195 | // Output: 196 | // good 197 | // good 198 | // good 199 | } 200 | 201 | func ExampleTraitSwitch() { 202 | err := errorx.TimeoutElapsed.NewWithNoMessage() 203 | 204 | switch errorx.TraitSwitch(err, errorx.Timeout()) { 205 | case errorx.Timeout(): 206 | fmt.Println("good") 207 | case errorx.CaseNoError(): 208 | fmt.Println("bad") 209 | default: 210 | fmt.Println("bad") 211 | } 212 | 213 | switch errorx.TraitSwitch(nil, errorx.Timeout()) { 214 | case errorx.Timeout(): 215 | fmt.Println("bad") 216 | case errorx.CaseNoError(): 217 | fmt.Println("good") 218 | default: 219 | fmt.Println("bad") 220 | } 221 | 222 | switch errorx.TraitSwitch(err, errorx.NotFound()) { 223 | case errorx.NotFound(): 224 | fmt.Println("bad") 225 | case errorx.CaseNoError(): 226 | fmt.Println("bad") 227 | default: 228 | fmt.Println("good") 229 | } 230 | 231 | // Output: 232 | // good 233 | // good 234 | // good 235 | } 236 | 237 | func nestedCall() error { 238 | return someFunc() 239 | } 240 | 241 | func someFunc() error { 242 | return errorx.AssertionFailed.New("example") 243 | } 244 | 245 | func getBodyAndError() ([]byte, error) { 246 | return nil, errorx.AssertionFailed.New("example") 247 | } 248 | 249 | func getDetailsFromBody(s []byte) (string, error) { 250 | return "", errorx.IllegalFormat.New(string(s)) 251 | } 252 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/joomcode/errorx 2 | 3 | go 1.11 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 // indirect 7 | github.com/stretchr/testify v1.4.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 8 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 12 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 13 | -------------------------------------------------------------------------------- /helper.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import "strings" 4 | 5 | func joinStringsIfNonEmpty(delimiter string, parts ...string) string { 6 | switch len(parts) { 7 | case 0: 8 | return "" 9 | case 1: 10 | return parts[0] 11 | case 2: 12 | if len(parts[0]) == 0 { 13 | return parts[1] 14 | } else if len(parts[1]) == 0 { 15 | return parts[0] 16 | } else { 17 | return parts[0] + delimiter + parts[1] 18 | } 19 | default: 20 | filteredParts := make([]string, 0, len(parts)) 21 | for _, part := range parts { 22 | if len(part) > 0 { 23 | filteredParts = append(filteredParts, part) 24 | } 25 | } 26 | 27 | return strings.Join(filteredParts, delimiter) 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /id.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "sync/atomic" 5 | ) 6 | 7 | var internalID uint64 8 | 9 | // nextInternalID creates next unique id for errorx entities. 10 | // All equality comparison should take id into account, lest there be some false positive matches. 11 | func nextInternalID() uint64 { 12 | return atomic.AddUint64(&internalID, 1) 13 | } 14 | -------------------------------------------------------------------------------- /modifier.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | // TypeModifier is a way to change a default behaviour for an error type, directly or via type hierarchy. 4 | // Modification is intentionally one-way, as it provides much more clarity. 5 | // If there is a modifier on a type or a namespace, all its descendants definitely have the same default behaviour. 6 | // If some of a subtypes must lack a specific modifier, then the modifier must be removed from the common ancestor. 7 | type TypeModifier int 8 | 9 | const ( 10 | // TypeModifierTransparent is a type modifier; an error type with such modifier creates transparent wrappers by default 11 | TypeModifierTransparent TypeModifier = 1 12 | // TypeModifierOmitStackTrace is a type modifier; an error type with such modifier omits the stack trace collection upon creation of an error instance 13 | TypeModifierOmitStackTrace TypeModifier = 2 14 | ) 15 | 16 | type modifiers interface { 17 | CollectStackTrace() bool 18 | Transparent() bool 19 | ReplaceWith(new modifiers) modifiers 20 | } 21 | 22 | var _ modifiers = noModifiers{} 23 | var _ modifiers = typeModifiers{} 24 | var _ modifiers = inheritedModifiers{} 25 | 26 | type noModifiers struct { 27 | } 28 | 29 | func (noModifiers) CollectStackTrace() bool { 30 | return true 31 | } 32 | 33 | func (noModifiers) Transparent() bool { 34 | return false 35 | } 36 | 37 | func (noModifiers) ReplaceWith(new modifiers) modifiers { 38 | return new 39 | } 40 | 41 | type typeModifiers struct { 42 | omitStackTrace bool 43 | transparent bool 44 | } 45 | 46 | func newTypeModifiers(modifiers ...TypeModifier) modifiers { 47 | m := typeModifiers{} 48 | for _, modifier := range modifiers { 49 | switch modifier { 50 | case TypeModifierOmitStackTrace: 51 | m.omitStackTrace = true 52 | case TypeModifierTransparent: 53 | m.transparent = true 54 | } 55 | } 56 | return m 57 | } 58 | 59 | func (m typeModifiers) CollectStackTrace() bool { 60 | return !m.omitStackTrace 61 | } 62 | 63 | func (m typeModifiers) Transparent() bool { 64 | return m.transparent 65 | } 66 | 67 | func (typeModifiers) ReplaceWith(new modifiers) modifiers { 68 | panic("attempt to modify type modifiers the second time") 69 | } 70 | 71 | type inheritedModifiers struct { 72 | parent modifiers 73 | override modifiers 74 | } 75 | 76 | func newInheritedModifiers(modifiers modifiers) modifiers { 77 | if _, ok := modifiers.(noModifiers); ok { 78 | return noModifiers{} 79 | } 80 | 81 | return inheritedModifiers{ 82 | parent: modifiers, 83 | override: noModifiers{}, 84 | } 85 | } 86 | 87 | func (m inheritedModifiers) CollectStackTrace() bool { 88 | return m.parent.CollectStackTrace() && m.override.CollectStackTrace() 89 | } 90 | 91 | func (m inheritedModifiers) Transparent() bool { 92 | return m.parent.Transparent() || m.override.Transparent() 93 | } 94 | 95 | func (m inheritedModifiers) ReplaceWith(new modifiers) modifiers { 96 | m.override = new 97 | return m 98 | } 99 | -------------------------------------------------------------------------------- /modifier_test.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | var ( 11 | modifierTestNamespace = NewNamespace("modifier") 12 | modifierTestNamespaceTransparent = NewNamespace("modifierTransparent").ApplyModifiers(TypeModifierTransparent) 13 | modifierTestNamespaceTransparentChild = modifierTestNamespaceTransparent.NewSubNamespace("child") 14 | modifierTestError = modifierTestNamespace.NewType("foo") 15 | modifierTestErrorNoTrace = modifierTestNamespace.NewType("bar").ApplyModifiers(TypeModifierOmitStackTrace) 16 | modifierTestErrorNoTraceChild = modifierTestErrorNoTrace.NewSubtype("child") 17 | modifierTestErrorTransparent = modifierTestNamespaceTransparent.NewType("simple") 18 | modifierTestErrorGrandchild = modifierTestNamespaceTransparentChild.NewType("all").ApplyModifiers(TypeModifierOmitStackTrace) 19 | ) 20 | 21 | func TestTypeModifier(t *testing.T) { 22 | t.Run("Default", func(t *testing.T) { 23 | err := modifierTestError.New("test") 24 | output := fmt.Sprintf("%+v", err) 25 | require.Contains(t, output, "errorx/modifier_test.go") 26 | }) 27 | 28 | t.Run("NoTrace", func(t *testing.T) { 29 | err := modifierTestErrorNoTrace.New("test") 30 | output := fmt.Sprintf("%+v", err) 31 | require.NotContains(t, output, "errorx/modifier_test.go") 32 | }) 33 | } 34 | 35 | func TestTypeModifierInheritance(t *testing.T) { 36 | t.Run("Type", func(t *testing.T) { 37 | err := modifierTestErrorNoTraceChild.New("test") 38 | output := fmt.Sprintf("%+v", err) 39 | require.NotContains(t, output, "errorx/modifier_test.go") 40 | }) 41 | 42 | t.Run("Namespace", func(t *testing.T) { 43 | err := modifierTestErrorTransparent.Wrap(AssertionFailed.New("test"), "boo") 44 | require.True(t, err.IsOfType(AssertionFailed)) 45 | }) 46 | 47 | t.Run("Deep", func(t *testing.T) { 48 | err := modifierTestErrorGrandchild.Wrap(AssertionFailed.New("test"), "boo") 49 | require.True(t, err.IsOfType(AssertionFailed)) 50 | 51 | err = modifierTestErrorGrandchild.New("test") 52 | output := fmt.Sprintf("%+v", err) 53 | require.NotContains(t, output, "errorx/modifier_test.go") 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /namespace.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import "fmt" 4 | 5 | // Namespace is a way go group a number of error types together, and each error type belongs to exactly one namespace. 6 | // Namespaces may form hierarchy, with child namespaces inheriting the traits and modifiers of a parent. 7 | // Those modifiers and traits are then passed upon all error types in the namespace. 8 | // In formatting, a dot notation is used, for example: 9 | // 10 | // namespace.sub_namespace.type.subtype 11 | // 12 | type Namespace struct { 13 | parent *Namespace 14 | id uint64 15 | name string 16 | traits []Trait 17 | modifiers modifiers 18 | } 19 | 20 | // NamespaceKey is a comparable descriptor of a Namespace. 21 | type NamespaceKey struct { 22 | id uint64 23 | } 24 | 25 | // NewNamespace defines a namespace with a name and, optionally, a number of inheritable traits. 26 | func NewNamespace(name string, traits ...Trait) Namespace { 27 | namespace := newNamespace(nil, name, traits...) 28 | globalRegistry.registerNamespace(namespace) 29 | return namespace 30 | } 31 | 32 | // NewSubNamespace defines a child namespace that inherits all that is defined for a parent and, optionally, adds some more. 33 | func (n Namespace) NewSubNamespace(name string, traits ...Trait) Namespace { 34 | namespace := newNamespace(&n, name, traits...) 35 | globalRegistry.registerNamespace(namespace) 36 | return namespace 37 | } 38 | 39 | // ApplyModifiers makes a one-time modification of defaults in error creation. 40 | func (n Namespace) ApplyModifiers(modifiers ...TypeModifier) Namespace { 41 | n.modifiers = n.modifiers.ReplaceWith(newTypeModifiers(modifiers...)) 42 | return n 43 | } 44 | 45 | // NewType creates a new type within a namespace that inherits all that is defined for namespace and, optionally, adds some more. 46 | func (n Namespace) NewType(typeName string, traits ...Trait) *Type { 47 | return NewType(n, typeName, traits...) 48 | } 49 | 50 | // Key returns a comparison key for namespace. 51 | func (n Namespace) Key() NamespaceKey { 52 | return NamespaceKey{ 53 | id: n.id, 54 | } 55 | } 56 | 57 | // IsNamespaceOf checks whether or not an error belongs either to this namespace or some of its sub-namespaces. 58 | func (n Namespace) IsNamespaceOf(t *Type) bool { 59 | namespace := t.namespace 60 | other := &namespace 61 | 62 | for other != nil { 63 | if n.Key() == other.Key() { 64 | return true 65 | } 66 | 67 | other = other.parent 68 | } 69 | 70 | return false 71 | } 72 | 73 | // FullName returns a full name of a namespace. 74 | func (n Namespace) FullName() string { 75 | return n.name 76 | } 77 | 78 | func (n Namespace) String() string { 79 | return n.name 80 | } 81 | 82 | // Parent returns the immediate parent namespace, if present. 83 | // The use of this function outside of a system layer that handles error types (see TypeSubscriber) is a code smell. 84 | func (n Namespace) Parent() *Namespace { 85 | return n.parent 86 | } 87 | 88 | func (n Namespace) collectTraits() map[Trait]bool { 89 | result := make(map[Trait]bool) 90 | namespace := &n 91 | for namespace != nil { 92 | for _, trait := range namespace.traits { 93 | result[trait] = true 94 | } 95 | 96 | namespace = namespace.parent 97 | } 98 | 99 | return result 100 | } 101 | 102 | func newNamespace(parent *Namespace, name string, traits ...Trait) Namespace { 103 | createName := func() string { 104 | if parent == nil { 105 | return name 106 | } 107 | return fmt.Sprintf("%s.%s", parent.FullName(), name) 108 | } 109 | 110 | createModifiers := func() modifiers { 111 | if parent == nil { 112 | return noModifiers{} 113 | } 114 | return newInheritedModifiers(parent.modifiers) 115 | } 116 | 117 | namespace := Namespace{ 118 | id: nextInternalID(), 119 | parent: parent, 120 | name: createName(), 121 | traits: append([]Trait(nil), traits...), 122 | modifiers: createModifiers(), 123 | } 124 | 125 | return namespace 126 | } 127 | -------------------------------------------------------------------------------- /namespace_test.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | var ( 10 | nsTest0 = NewNamespace("nsTest0") 11 | nsTest1 = NewNamespace("nsTest1") 12 | nsTest1Child = nsTest1.NewSubNamespace("child") 13 | nsTestET0 = nsTest0.NewType("type0") 14 | nsTestET1 = nsTest1.NewType("type1") 15 | nsTestET1Child = nsTestET1.NewSubtype("child") 16 | nsTestChild1ET = nsTest1Child.NewType("type") 17 | nsTestChild1ETChild = nsTestChild1ET.NewSubtype("child") 18 | ) 19 | 20 | func TestNamespaceName(t *testing.T) { 21 | require.EqualValues(t, "nsTest1", nsTest1.FullName()) 22 | require.EqualValues(t, "nsTest1.child", nsTest1Child.FullName()) 23 | } 24 | 25 | func TestIsNamespaceOf(t *testing.T) { 26 | require.True(t, nsTest0.IsNamespaceOf(nsTestET0)) 27 | require.False(t, nsTest1.IsNamespaceOf(nsTestET0)) 28 | require.False(t, nsTest0.IsNamespaceOf(nsTestET1)) 29 | require.True(t, nsTest1.IsNamespaceOf(nsTestET1)) 30 | } 31 | 32 | func TestNamespaceSubtype(t *testing.T) { 33 | require.False(t, nsTest0.IsNamespaceOf(nsTestET1Child)) 34 | require.True(t, nsTest1.IsNamespaceOf(nsTestET1Child)) 35 | } 36 | 37 | func TestSubNamespace(t *testing.T) { 38 | require.False(t, nsTest1Child.IsNamespaceOf(nsTestET1)) 39 | require.True(t, nsTest1Child.IsNamespaceOf(nsTestChild1ET)) 40 | require.True(t, nsTest1Child.IsNamespaceOf(nsTestChild1ETChild)) 41 | } 42 | 43 | 44 | func TestRootNamespace(t *testing.T) { 45 | require.Equal(t, nsTest1, nsTestChild1ET.NewWithNoMessage().Type().RootNamespace()) 46 | } 47 | 48 | func TestNamespace(t *testing.T) { 49 | require.Equal(t, nsTest1Child, nsTestChild1ET.NewWithNoMessage().Type().Namespace()) 50 | } 51 | 52 | func TestSubTypeNamespaceFullName(t *testing.T) { 53 | require.Equal(t, "nsTest1.child", nsTestChild1ETChild.Namespace().FullName()) 54 | } 55 | -------------------------------------------------------------------------------- /panic.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import "fmt" 4 | 5 | // Panic is an alternative to the built-in panic call. 6 | // When calling panic as a reaction to error, prefer this function over vanilla panic(). 7 | // If err happens to be an errorx error, it may hold the original stack trace of the issue. 8 | // With panic(err), this information may be lost if panic is handled by the default handler. 9 | // With errorx.Panic(err), all data is preserved regardless of the handle mechanism. 10 | // It can be recovered either from default panic message, recover() result or ErrorFromPanic() function. 11 | // 12 | // Even if err stack trace is exactly the same as default panic trace, this can be tolerated, 13 | // as panics must not be a way to report conventional errors and are therefore rare. 14 | // With this in mind, it is better to err on the side of completeness rather than brevity. 15 | // 16 | // This function never returns, but the signature may be used for convenience: 17 | // 18 | // return nil, errorx.Panic(err) 19 | // panic(errorx.Panic(err)) 20 | // 21 | func Panic(err error) error { 22 | panic(newPanicErrorWrapper(err)) 23 | } 24 | 25 | // ErrorFromPanic recovers the original error from panic, best employed along with Panic() function from the same package. 26 | // The original error, if present, typically holds more relevant data 27 | // than a combination of panic message and the stack trace which can be collected after recover(). 28 | // 29 | // More importantly, it allows for greater composability, 30 | // if ever there is a need to recover from panic and pass the error information forwards in its proper form. 31 | // 32 | // Note that panic is not a proper means to report errors, 33 | // so this mechanism should never be used where a error based control flow is at all possible. 34 | func ErrorFromPanic(recoverResult interface{}) (error, bool) { 35 | err, ok := recoverResult.(error) 36 | if !ok { 37 | return nil, false 38 | } 39 | 40 | if wrapper, ok := err.(*panicErrorWrapper); ok { 41 | return wrapper.inner, true 42 | } 43 | 44 | return err, true 45 | } 46 | 47 | func newPanicErrorWrapper(err error) *panicErrorWrapper { 48 | return &panicErrorWrapper{ 49 | inner: NewErrorBuilder(panicPayloadWrap). 50 | WithConditionallyFormattedMessage("panic"). 51 | WithCause(err). 52 | EnhanceStackTrace(). 53 | Create(), 54 | } 55 | } 56 | 57 | // panicErrorWrapper is designed for the original stack trace not to be lost in any way it may be handled 58 | type panicErrorWrapper struct { 59 | inner error 60 | } 61 | 62 | func (w *panicErrorWrapper) Error() string { 63 | return fmt.Sprintf("%+v", w.inner) 64 | } 65 | 66 | func (w *panicErrorWrapper) String() string { 67 | return w.Error() 68 | } 69 | 70 | // Only required to transform panic into error while preserving the stack trace 71 | var panicPayloadWrap = syntheticErrors.NewType("panic").ApplyModifiers(TypeModifierTransparent) 72 | -------------------------------------------------------------------------------- /panic_test.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestPanic(t *testing.T) { 13 | 14 | defer func() { 15 | r := recover() 16 | require.NotNil(t, r) 17 | output := fmt.Sprintf("%v", r) 18 | 19 | require.Contains(t, output, "errorx.funcWithErr()", output) 20 | }() 21 | 22 | Panic(funcWithErr()) 23 | } 24 | 25 | func TestPanicErrorx(t *testing.T) { 26 | 27 | defer func() { 28 | r := recover() 29 | require.NotNil(t, r) 30 | output := fmt.Sprintf("%v", r) 31 | 32 | require.Contains(t, output, "awful", output) 33 | require.Contains(t, output, "errorx.funcWithBadPanic()", output) 34 | }() 35 | 36 | funcWithBadPanic() 37 | } 38 | 39 | func TestPanicRecover(t *testing.T) { 40 | 41 | defer func() { 42 | r := recover() 43 | require.NotNil(t, r) 44 | 45 | err, ok := ErrorFromPanic(r) 46 | require.True(t, ok) 47 | 48 | output := fmt.Sprintf("%+v", err) 49 | require.Contains(t, output, "errorx.funcWithErr()", output) 50 | require.Contains(t, output, "bad", output) 51 | require.True(t, IsOfType(err, testType)) 52 | }() 53 | 54 | Panic(funcWithErr()) 55 | } 56 | 57 | func TestPanicRecoverNoTrace(t *testing.T) { 58 | 59 | defer func() { 60 | r := recover() 61 | require.NotNil(t, r) 62 | 63 | err, ok := ErrorFromPanic(r) 64 | require.True(t, ok) 65 | 66 | output := fmt.Sprintf("%+v", err) 67 | require.NotContains(t, output, "errorx.funcWithErrNoTrace()", output) 68 | require.Contains(t, output, "errorx.funcWithPanicNoTrace()", output) 69 | require.Contains(t, output, "silent", output) 70 | require.True(t, IsOfType(err, testType)) 71 | }() 72 | 73 | funcWithPanicNoTrace() 74 | } 75 | 76 | func TestPanicRecoverNoErrorx(t *testing.T) { 77 | 78 | defer func() { 79 | r := recover() 80 | require.NotNil(t, r) 81 | 82 | err, ok := ErrorFromPanic(r) 83 | require.True(t, ok) 84 | 85 | output := fmt.Sprintf("%+v", err) 86 | require.NotContains(t, output, "errorx.funcWithBadErr()", output) 87 | require.Contains(t, output, "errorx.funcWithBadPanic()", output) 88 | require.Contains(t, output, "awful", output) 89 | require.False(t, IsOfType(err, testType)) 90 | }() 91 | 92 | funcWithBadPanic() 93 | } 94 | 95 | func funcWithErr() error { 96 | return testType.New("bad") 97 | } 98 | 99 | func funcWithPanicNoTrace() { 100 | Panic(funcWithErrNoTrace()) 101 | } 102 | 103 | func funcWithErrNoTrace() error { 104 | return testTypeSilent.New("silent") 105 | } 106 | 107 | func funcWithBadPanic() { 108 | Panic(funcWithBadErr()) 109 | } 110 | 111 | func funcWithBadErr() error { 112 | return errors.New("awful") 113 | } 114 | 115 | func TestPanicChain(t *testing.T) { 116 | ch0 := make(chan error, 1) 117 | ch1 := make(chan error, 1) 118 | 119 | go doMischief(ch1) 120 | go doMoreMischief(ch0, ch1) 121 | 122 | select { 123 | case err := <-ch0: 124 | require.Error(t, err) 125 | require.False(t, IsOfType(err, AssertionFailed)) 126 | output := fmt.Sprintf("%+v", err) 127 | require.Contains(t, output, "mischiefProper", output) 128 | require.Contains(t, output, "mischiefAsPanic", output) 129 | require.Contains(t, output, "doMischief", output) 130 | require.Contains(t, output, "handleMischief", output) 131 | require.NotContains(t, output, "doMoreMischief", output) // stack trace is only enhanced in Panic, not in user code 132 | t.Log(output) 133 | case <-time.After(time.Second): 134 | require.Fail(t, "expected error") 135 | } 136 | } 137 | 138 | func doMoreMischief(ch0 chan error, ch1 chan error) { 139 | defer func() { 140 | if e := recover(); e != nil { 141 | err, ok := ErrorFromPanic(e) 142 | if ok { 143 | ch0 <- Decorate(err, "hop 2") 144 | return 145 | } 146 | } 147 | ch0 <- AssertionFailed.New("test failed") 148 | }() 149 | 150 | handleMischief(ch1) 151 | } 152 | 153 | func handleMischief(ch chan error) { 154 | err := <-ch 155 | Panic(Decorate(err, "handle")) 156 | } 157 | 158 | func doMischief(ch chan error) { 159 | defer func() { 160 | if e := recover(); e != nil { 161 | err, ok := ErrorFromPanic(e) 162 | if ok { 163 | ch <- Decorate(err, "hop 1") 164 | return 165 | } 166 | } 167 | ch <- AssertionFailed.New("test failed") 168 | }() 169 | 170 | mischiefAsPanic() 171 | } 172 | 173 | func mischiefAsPanic() { 174 | Panic(mischiefProper()) 175 | } 176 | 177 | func mischiefProper() error { 178 | return ExternalError.New("mischief") 179 | } 180 | -------------------------------------------------------------------------------- /property.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // Property is a key to a dynamic property of an error. 8 | // Property value belongs to an error instance only, never inherited from a type. 9 | // Property visibility is hindered by Wrap, preserved by Decorate. 10 | type Property struct { 11 | *property // Property is compared by this pointer. 12 | } 13 | 14 | type property struct { 15 | label string 16 | printable bool 17 | } 18 | 19 | // RegisterProperty registers a new property key. 20 | // It is used both to add a dynamic property to an error instance, and to extract property value back from error. 21 | func RegisterProperty(label string) Property { 22 | return newProperty(label, false) 23 | } 24 | 25 | // RegisterPrintableProperty registers a new property key for informational value. 26 | // It is used both to add a dynamic property to an error instance, and to extract property value back from error. 27 | // Printable property will be included in Error() message, both name and value. 28 | func RegisterPrintableProperty(label string) Property { 29 | return newProperty(label, true) 30 | } 31 | 32 | // PropertyContext is a context property, value is expected to be of context.Context type. 33 | func PropertyContext() Property { 34 | return propertyContext 35 | } 36 | 37 | // PropertyPayload is a payload property, value may contain user defined structure with arbitrary data passed along with an error. 38 | func PropertyPayload() Property { 39 | return propertyPayload 40 | } 41 | 42 | // WithContext is a statically typed helper to add a context property to an error. 43 | func WithContext(err *Error, ctx context.Context) *Error { 44 | return err.WithProperty(PropertyContext(), ctx) 45 | } 46 | 47 | // ExtractContext is a statically typed helper to extract a context property from an error. 48 | func ExtractContext(err error) (context.Context, bool) { 49 | rawCtx, ok := ExtractProperty(err, PropertyContext()) 50 | if !ok { 51 | return nil, false 52 | } 53 | 54 | return rawCtx.(context.Context), true 55 | } 56 | 57 | // WithPayload is a helper to add a payload property to an error. 58 | func WithPayload(err *Error, payload interface{}) *Error { 59 | return err.WithProperty(PropertyPayload(), payload) 60 | } 61 | 62 | // ExtractPayload is a helper to extract a payload property from an error. 63 | func ExtractPayload(err error) (interface{}, bool) { 64 | return ExtractProperty(err, PropertyPayload()) 65 | } 66 | 67 | // ExtractProperty attempts to extract a property value by a provided key. 68 | // A property may belong to this error or be extracted from the original cause. 69 | func ExtractProperty(err error, key Property) (interface{}, bool) { 70 | typedErr := Cast(err) 71 | if typedErr == nil { 72 | return nil, false 73 | } 74 | 75 | return typedErr.Property(key) 76 | } 77 | 78 | var ( 79 | propertyContext = RegisterProperty("ctx") 80 | propertyPayload = RegisterProperty("payload") 81 | propertyUnderlying = RegisterProperty("underlying") 82 | ) 83 | 84 | func newProperty(label string, printable bool) Property { 85 | p := Property{ 86 | &property{ 87 | label: label, 88 | printable: printable, 89 | }, 90 | } 91 | return p 92 | } 93 | 94 | // propertyMap represents map of properties. 95 | // Compared to builtin type, it uses less allocations and reallocations on copy. 96 | // It is implemented as a simple linked list. 97 | type propertyMap struct { 98 | p Property 99 | value interface{} 100 | next *propertyMap 101 | } 102 | 103 | func (pm *propertyMap) with(p Property, value interface{}) *propertyMap { 104 | return &propertyMap{p: p, value: value, next: pm} 105 | } 106 | 107 | func (pm *propertyMap) get(p Property) (value interface{}, ok bool) { 108 | for pm != nil { 109 | if pm.p == p { 110 | return pm.value, true 111 | } 112 | pm = pm.next 113 | } 114 | return nil, false 115 | } 116 | -------------------------------------------------------------------------------- /property_test.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestNoProperty(t *testing.T) { 12 | t.Run("Simple", func(t *testing.T) { 13 | err := testType.New("test") 14 | property, ok := err.Property(PropertyPayload()) 15 | require.False(t, ok) 16 | require.Nil(t, property) 17 | }) 18 | 19 | t.Run("Decorated", func(t *testing.T) { 20 | err := testType.New("test") 21 | err = Decorate(err, "oops") 22 | property, ok := err.Property(PropertyPayload()) 23 | require.False(t, ok) 24 | require.Nil(t, property) 25 | }) 26 | 27 | t.Run("Helper", func(t *testing.T) { 28 | err := testType.New("test") 29 | property, ok := ExtractPayload(err) 30 | require.False(t, ok) 31 | require.Nil(t, property) 32 | }) 33 | } 34 | 35 | var testProperty0 = RegisterProperty("test0") 36 | var testProperty1 = RegisterProperty("test1") 37 | var testInfoProperty2 = RegisterPrintableProperty("prop2") 38 | var testInfoProperty3 = RegisterPrintableProperty("prop3") 39 | 40 | func TestProperty(t *testing.T) { 41 | t.Run("Different", func(t *testing.T) { 42 | err := testType.New("test").WithProperty(testProperty0, 42) 43 | 44 | property0, ok := err.Property(testProperty0) 45 | require.True(t, ok) 46 | require.EqualValues(t, 42, property0) 47 | 48 | property1, ok := err.Property(testProperty1) 49 | require.False(t, ok) 50 | require.Nil(t, property1) 51 | }) 52 | 53 | t.Run("Wrapped", func(t *testing.T) { 54 | err := testType.New("test").WithProperty(testProperty0, 42) 55 | err = Decorate(err, "oops") 56 | err = testTypeBar1.Wrap(err, "wrapped") 57 | 58 | property0, ok := err.Property(testProperty0) 59 | require.False(t, ok) 60 | require.Nil(t, property0) 61 | 62 | property1, ok := err.Property(testProperty1) 63 | require.False(t, ok) 64 | require.Nil(t, property1) 65 | }) 66 | 67 | t.Run("Decorated", func(t *testing.T) { 68 | err := testType.New("test").WithProperty(testProperty0, 42) 69 | err = Decorate(err, "oops") 70 | err = Decorate(err, "bad") 71 | 72 | property0, ok := err.Property(testProperty0) 73 | require.True(t, ok) 74 | require.EqualValues(t, 42, property0) 75 | 76 | property1, ok := err.Property(testProperty1) 77 | require.False(t, ok) 78 | require.Nil(t, property1) 79 | }) 80 | 81 | t.Run("FromCause", func(t *testing.T) { 82 | err := testType.New("test").WithProperty(testProperty0, 42) 83 | err = Decorate(err, "oops") 84 | err = Decorate(err, "bad").WithProperty(testProperty1, "-1") 85 | 86 | property0, ok := err.Property(testProperty0) 87 | require.True(t, ok) 88 | require.EqualValues(t, 42, property0) 89 | 90 | property1, ok := err.Property(testProperty1) 91 | require.True(t, ok) 92 | require.EqualValues(t, "-1", property1) 93 | }) 94 | 95 | t.Run("OverrideCause", func(t *testing.T) { 96 | err := testType.New("test").WithProperty(testProperty0, 42) 97 | err = Decorate(err, "oops") 98 | 99 | property0, ok := err.Property(testProperty0) 100 | require.True(t, ok) 101 | require.EqualValues(t, 42, property0) 102 | 103 | err = Decorate(err, "bad").WithProperty(testProperty0, "-1") 104 | 105 | property0, ok = err.Property(testProperty0) 106 | require.True(t, ok) 107 | require.EqualValues(t, "-1", property0) 108 | 109 | property1, ok := err.Property(testProperty1) 110 | require.False(t, ok) 111 | require.Nil(t, property1) 112 | }) 113 | } 114 | 115 | func TestPrintableProperty(t *testing.T) { 116 | err := testTypeSilent.New("test").WithProperty(testInfoProperty2, "hello world") 117 | t.Run("Simple", func(t *testing.T) { 118 | assert.Equal(t, "foo.bar.silent: test {prop2: hello world}", err.Error()) 119 | }) 120 | 121 | t.Run("Overwrite", func(t *testing.T) { 122 | err := err.WithProperty(testInfoProperty2, "cruel world") 123 | assert.Equal(t, "foo.bar.silent: test {prop2: cruel world}", err.Error()) 124 | }) 125 | 126 | t.Run("AddMore", func(t *testing.T) { 127 | err := err.WithProperty(testInfoProperty3, struct{ a int }{1}) 128 | assert.Equal(t, "foo.bar.silent: test {prop3: {1}, prop2: hello world}", err.Error()) 129 | }) 130 | 131 | t.Run("NonPrintableIsInvisible", func(t *testing.T) { 132 | err := err.WithProperty(testProperty0, "nah") 133 | assert.Equal(t, "foo.bar.silent: test {prop2: hello world}", err.Error()) 134 | }) 135 | 136 | t.Run("WithUnderlying", func(t *testing.T) { 137 | err := err.WithUnderlyingErrors(testTypeSilent.New("underlying")) 138 | assert.Equal(t, "foo.bar.silent: test {prop2: hello world} (hidden: foo.bar.silent: underlying)", err.Error()) 139 | }) 140 | 141 | err2 := Decorate(err, "oops") 142 | t.Run("Decorate", func(t *testing.T) { 143 | assert.Equal(t, "oops, cause: foo.bar.silent: test {prop2: hello world}", err2.Error()) 144 | }) 145 | 146 | t.Run("DecorateAndAddMore", func(t *testing.T) { 147 | err := err2.WithProperty(testInfoProperty3, struct{ a int }{1}) 148 | assert.Equal(t, "oops {prop3: {1}}, cause: foo.bar.silent: test {prop2: hello world}", err.Error()) 149 | }) 150 | 151 | t.Run("DecorateAndAddSame", func(t *testing.T) { 152 | err := err2.WithProperty(testInfoProperty2, "cruel world") 153 | assert.Equal(t, "oops {prop2: cruel world}, cause: foo.bar.silent: test {prop2: hello world}", err.Error()) 154 | }) 155 | } 156 | 157 | func BenchmarkAllocProperty(b *testing.B) { 158 | const N = 9 159 | var properties = []Property{} 160 | for j := 0; j < N; j++ { 161 | n := fmt.Sprintf("props%d", j) 162 | properties = append(properties, RegisterProperty(n)) 163 | b.Run(n, func(b *testing.B) { 164 | for k := 0; k < b.N; k++ { 165 | err := testTypeSilent.New("test") 166 | for i := 0; i < j; i++ { 167 | err = err.WithProperty(properties[i], 42) 168 | } 169 | } 170 | }) 171 | } 172 | } 173 | 174 | var sum int 175 | 176 | func BenchmarkGetProperty(b *testing.B) { 177 | const N = 9 178 | var properties = []Property{} 179 | for j := 0; j < N; j++ { 180 | n := fmt.Sprintf("props%d", j) 181 | properties = append(properties, RegisterProperty(n)) 182 | b.Run(n, func(b *testing.B) { 183 | err := testTypeSilent.New("test") 184 | for i := 0; i < j; i++ { 185 | err = err.WithProperty(properties[i], 42) 186 | } 187 | for k := 0; k < b.N; k++ { 188 | v, ok := err.Property(testProperty0) 189 | if ok { 190 | sum += v.(int) 191 | } 192 | v, ok = err.Property(properties[j]) 193 | if ok { 194 | sum += v.(int) 195 | } 196 | v, ok = err.Property(properties[0]) 197 | if ok { 198 | sum += v.(int) 199 | } 200 | } 201 | }) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /readme.go: -------------------------------------------------------------------------------- 1 | // Package errorx provides error implementation and error-related utilities. 2 | // 3 | // Conventional approach towards errors in Go is quite limited. 4 | // The typical case implies an error being created at some point: 5 | // 6 | // return errors.New("now this is unfortunate") 7 | // 8 | // Then being passed along with a no-brainer: 9 | // 10 | // if err != nil { 11 | // return err 12 | // } 13 | // 14 | // And, finally, handled by printing it to the log file: 15 | // 16 | // log.Errorf("Error: %s", err) 17 | // 18 | // This approach is simple, but quite often it is not enough. 19 | // There is a need to add context information to error, to check or hide its properties. 20 | // If all else fails, it pays to have a stack trace printed along with error text. 21 | // 22 | // Syntax 23 | // 24 | // The code above could be modified in this fashion: 25 | // 26 | // return errorx.IllegalState.New("unfortunate") 27 | // 28 | // if err != nil { 29 | // return errorx.Decorate(err, "this could be so much better") 30 | // } 31 | // 32 | // log.Errorf("Error: %+v", err) 33 | // 34 | // Here errorx.Decorate is used to add more information, 35 | // and syntax like errorx.IsOfType can still be used to check the original error. 36 | // This error also holds a stack trace captured at the point of creation. 37 | // With errorx syntax, any of this may be customized: stack trace can be omitted, error type can be hidden. 38 | // Type can be further customized with Traits, and error with Properties. 39 | // Package provides utility functions to compose, switch over, check, and ignore errors based on their types and properties. 40 | // 41 | // See documentation for Error, Type and Namespace for more details. 42 | package errorx 43 | -------------------------------------------------------------------------------- /registry.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import "sync" 4 | 5 | // TypeSubscriber is an interface to receive callbacks on the registered error namespaces and types. 6 | // This may be used to create a user-defined registry, for example, to check if all type names are unique. 7 | // ISSUE: if .ApplyModifiers is called for a type/namespace, callback still receives a value without those modifiers. 8 | type TypeSubscriber interface { 9 | // OnNamespaceCreated is called exactly once for each namespace 10 | OnNamespaceCreated(namespace Namespace) 11 | // OnTypeCreated is called exactly once for each type 12 | OnTypeCreated(t *Type) 13 | } 14 | 15 | // RegisterTypeSubscriber adds a new TypeSubscriber. 16 | // A subscriber is guaranteed to receive callbacks for all namespaces and types. 17 | // If a type is already registered at the moment of subscription, a callback for this type is called immediately. 18 | func RegisterTypeSubscriber(s TypeSubscriber) { 19 | globalRegistry.registerTypeSubscriber(s) 20 | } 21 | 22 | type registry struct { 23 | mu sync.Mutex 24 | subscribers []TypeSubscriber 25 | knownNamespaces []Namespace 26 | knownTypes []*Type 27 | } 28 | 29 | var globalRegistry = ®istry{} 30 | 31 | func (r *registry) registerNamespace(namespace Namespace) { 32 | r.mu.Lock() 33 | defer r.mu.Unlock() 34 | 35 | r.knownNamespaces = append(r.knownNamespaces, namespace) 36 | for _, s := range r.subscribers { 37 | s.OnNamespaceCreated(namespace) 38 | } 39 | } 40 | 41 | func (r *registry) registerType(t *Type) { 42 | r.mu.Lock() 43 | defer r.mu.Unlock() 44 | 45 | r.knownTypes = append(r.knownTypes, t) 46 | for _, s := range r.subscribers { 47 | s.OnTypeCreated(t) 48 | } 49 | } 50 | 51 | func (r *registry) registerTypeSubscriber(s TypeSubscriber) { 52 | for _, ns := range r.knownNamespaces { 53 | s.OnNamespaceCreated(ns) 54 | } 55 | 56 | for _, t := range r.knownTypes { 57 | s.OnTypeCreated(t) 58 | } 59 | 60 | r.subscribers = append(r.subscribers, s) 61 | } 62 | -------------------------------------------------------------------------------- /registry_test.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestRegistry(t *testing.T) { 10 | s := &testSubscriber{} 11 | RegisterTypeSubscriber(s) 12 | 13 | require.Contains(t, s.namespaces, CommonErrors.Key()) 14 | require.Contains(t, s.types, AssertionFailed) 15 | 16 | ns := NewNamespace("TestRegistry") 17 | require.Contains(t, s.namespaces, ns.Key()) 18 | 19 | errorType := ns.NewType("Test") 20 | require.Contains(t, s.types, errorType) 21 | } 22 | 23 | type testSubscriber struct { 24 | types []*Type 25 | namespaces []NamespaceKey 26 | } 27 | 28 | func (s *testSubscriber) OnNamespaceCreated(namespace Namespace) { 29 | s.namespaces = append(s.namespaces, namespace.Key()) 30 | } 31 | 32 | func (s *testSubscriber) OnTypeCreated(t *Type) { 33 | s.types = append(s.types, t) 34 | } 35 | -------------------------------------------------------------------------------- /stackframe.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "runtime" 5 | ) 6 | 7 | type frame interface { 8 | Function() string 9 | File() string 10 | Line() int 11 | } 12 | 13 | type frameHelper struct { 14 | } 15 | 16 | var frameHelperSingleton = &frameHelper{} 17 | 18 | type defaultFrame struct { 19 | frame *runtime.Frame 20 | } 21 | 22 | func (f *defaultFrame) Function() string { 23 | return f.frame.Function 24 | } 25 | 26 | func (f *defaultFrame) File() string { 27 | return f.frame.File 28 | } 29 | 30 | func (f *defaultFrame) Line() int { 31 | return f.frame.Line 32 | } 33 | 34 | func (c *frameHelper) GetFrames(pcs []uintptr) []frame { 35 | frames := runtime.CallersFrames(pcs[:]) 36 | result := make([]frame, 0, len(pcs)) 37 | 38 | var rawFrame runtime.Frame 39 | next := true 40 | for next { 41 | rawFrame, next = frames.Next() 42 | frameCopy := rawFrame 43 | frame := &defaultFrame{&frameCopy} 44 | result = append(result, frame) 45 | } 46 | 47 | return result 48 | } 49 | -------------------------------------------------------------------------------- /stacktrace.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "runtime" 7 | "strconv" 8 | "sync" 9 | "sync/atomic" 10 | ) 11 | 12 | // StackTraceFilePathTransformer is a used defined transformer for file path in stack trace output. 13 | type StackTraceFilePathTransformer func(string) string 14 | 15 | // InitializeStackTraceTransformer provides a transformer to be used in formatting of all the errors. 16 | // It is OK to leave it alone, stack trace will retain its exact original information. 17 | // This feature may be beneficial, however, if a shortening of file path will make it more convenient to use. 18 | // One of such examples is to transform a project-related path from absolute to relative and thus more IDE-friendly. 19 | // 20 | // NB: error is returned if a transformer was already registered. 21 | // Transformer is changed nonetheless, the old one is returned along with an error. 22 | // User is at liberty to either ignore it, panic, reinstate the old transformer etc. 23 | func InitializeStackTraceTransformer(transformer StackTraceFilePathTransformer) (StackTraceFilePathTransformer, error) { 24 | stackTraceTransformer.mu.Lock() 25 | defer stackTraceTransformer.mu.Unlock() 26 | 27 | old := stackTraceTransformer.transform.Load().(StackTraceFilePathTransformer) 28 | stackTraceTransformer.transform.Store(transformer) 29 | 30 | if stackTraceTransformer.initialized { 31 | return old, InitializationFailed.New("stack trace transformer was already set up: %#v", old) 32 | } 33 | 34 | stackTraceTransformer.initialized = true 35 | return nil, nil 36 | } 37 | 38 | var stackTraceTransformer = struct { 39 | mu *sync.Mutex 40 | transform *atomic.Value 41 | initialized bool 42 | }{ 43 | &sync.Mutex{}, 44 | &atomic.Value{}, 45 | false, 46 | } 47 | 48 | func init() { 49 | stackTraceTransformer.transform.Store(transformStackTraceLineNoop) 50 | } 51 | 52 | var transformStackTraceLineNoop StackTraceFilePathTransformer = func(line string) string { 53 | return line 54 | } 55 | 56 | const ( 57 | stackTraceDepth = 128 58 | // tuned so that in all control paths of error creation the first frame is useful 59 | // that is, the frame where New/Wrap/Decorate etc. are called; see TestStackTraceStart 60 | skippedFrames = 6 61 | ) 62 | 63 | func collectStackTrace() *stackTrace { 64 | var pc [stackTraceDepth]uintptr 65 | depth := runtime.Callers(skippedFrames, pc[:]) 66 | return &stackTrace{ 67 | pc: pc[:depth], 68 | } 69 | } 70 | 71 | type stackTrace struct { 72 | pc []uintptr 73 | causeStackTrace *stackTrace 74 | } 75 | 76 | func (st *stackTrace) enhanceWithCause(causeStackTrace *stackTrace) { 77 | st.causeStackTrace = causeStackTrace 78 | } 79 | 80 | func (st *stackTrace) Format(s fmt.State, verb rune) { 81 | if st == nil { 82 | return 83 | } 84 | 85 | switch verb { 86 | case 'v', 's': 87 | st.formatStackTrace(s) 88 | 89 | if st.causeStackTrace != nil { 90 | io.WriteString(s, "\n ---------------------------------- ") 91 | st.causeStackTrace.Format(s, verb) 92 | } 93 | } 94 | } 95 | 96 | func (st *stackTrace) formatStackTrace(s fmt.State) { 97 | transformLine := stackTraceTransformer.transform.Load().(StackTraceFilePathTransformer) 98 | 99 | pc, cropped := st.deduplicateFramesWithCause() 100 | if len(pc) == 0 { 101 | return 102 | } 103 | 104 | frames := frameHelperSingleton.GetFrames(pc) 105 | for _, frame := range frames { 106 | io.WriteString(s, "\n at ") 107 | io.WriteString(s, frame.Function()) 108 | io.WriteString(s, "()\n\t") 109 | io.WriteString(s, transformLine(frame.File())) 110 | io.WriteString(s, ":") 111 | io.WriteString(s, strconv.Itoa(frame.Line())) 112 | } 113 | 114 | if cropped > 0 { 115 | io.WriteString(s, "\n ...\n (") 116 | io.WriteString(s, strconv.Itoa(cropped)) 117 | io.WriteString(s, " duplicated frames)") 118 | } 119 | } 120 | 121 | func (st *stackTrace) deduplicateFramesWithCause() ([]uintptr, int) { 122 | if st.causeStackTrace == nil { 123 | return st.pc, 0 124 | } 125 | 126 | pc := st.pc 127 | causePC := st.causeStackTrace.pc 128 | 129 | for i := 1; i <= len(pc) && i <= len(causePC); i++ { 130 | if pc[len(pc)-i] != causePC[len(causePC)-i] { 131 | return pc[:len(pc)-i], i - 1 132 | } 133 | } 134 | 135 | return nil, len(pc) 136 | } 137 | -------------------------------------------------------------------------------- /stacktrace_test.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestStackTraceStart(t *testing.T) { 16 | t.Run("New", func(t *testing.T) { 17 | err := AssertionFailed.New("achtung") 18 | output := fmt.Sprintf("%+v", err) 19 | require.Contains(t, output, "achtung", output) 20 | require.NotContains(t, output, "New()", output) 21 | require.Contains(t, output, "TestStackTraceStart", output) 22 | }) 23 | 24 | t.Run("NewWithNoMessage", func(t *testing.T) { 25 | err := AssertionFailed.NewWithNoMessage() 26 | output := fmt.Sprintf("%+v", err) 27 | require.NotContains(t, output, "NewWithNoMessage()", output) 28 | require.Contains(t, output, "TestStackTraceStart", output) 29 | }) 30 | 31 | t.Run("Wrap", func(t *testing.T) { 32 | err := AssertionFailed.Wrap(TimeoutElapsed.NewWithNoMessage(), "achtung") 33 | output := fmt.Sprintf("%+v", err) 34 | require.Contains(t, output, "achtung", output) 35 | require.NotContains(t, output, "Wrap()", output) 36 | require.Contains(t, output, "TestStackTraceStart", output) 37 | }) 38 | 39 | t.Run("WrapWithNoMessage", func(t *testing.T) { 40 | err := AssertionFailed.WrapWithNoMessage(TimeoutElapsed.NewWithNoMessage()) 41 | output := fmt.Sprintf("%+v", err) 42 | require.NotContains(t, output, "WrapWithNoMessage()", output) 43 | require.Contains(t, output, "TestStackTraceStart", output) 44 | }) 45 | 46 | t.Run("WrapAddStackTrace", func(t *testing.T) { 47 | err := testTypeSilent.NewWithNoMessage() 48 | output := fmt.Sprintf("%+v", err) 49 | require.NotContains(t, output, "TestStackTraceStart", output) 50 | 51 | err = AssertionFailed.Wrap(err, "achtung") 52 | output = fmt.Sprintf("%+v", err) 53 | require.Contains(t, output, "achtung", output) 54 | require.NotContains(t, output, "Wrap()", output) 55 | require.Contains(t, output, "TestStackTraceStart", output) 56 | }) 57 | 58 | t.Run("EnhanceStackTrace", func(t *testing.T) { 59 | err := EnhanceStackTrace(AssertionFailed.New("achtung"), "enhance") 60 | output := fmt.Sprintf("%+v", err) 61 | require.Contains(t, output, "achtung", output) 62 | require.NotContains(t, output, "EnhanceStackTrace()", output) 63 | require.Contains(t, output, "TestStackTraceStart", output) 64 | }) 65 | 66 | t.Run("EnhanceStackTraceWithRaw", func(t *testing.T) { 67 | err := EnhanceStackTrace(errors.New("achtung"), "enhance") 68 | output := fmt.Sprintf("%+v", err) 69 | require.Contains(t, output, "achtung", output) 70 | require.NotContains(t, output, "EnhanceStackTrace()", output) 71 | require.Contains(t, output, "TestStackTraceStart", output) 72 | }) 73 | 74 | t.Run("Decorate", func(t *testing.T) { 75 | err := Decorate(AssertionFailed.New("achtung"), "enhance") 76 | output := fmt.Sprintf("%+v", err) 77 | require.Contains(t, output, "achtung", output) 78 | require.NotContains(t, output, "Decorate()", output) 79 | require.Contains(t, output, "TestStackTraceStart", output) 80 | }) 81 | 82 | t.Run("DecorateWithRaw", func(t *testing.T) { 83 | err := Decorate(errors.New("achtung"), "enhance") 84 | output := fmt.Sprintf("%+v", err) 85 | require.Contains(t, output, "achtung", output) 86 | require.NotContains(t, output, "Decorate()", output) 87 | require.Contains(t, output, "TestStackTraceStart", output) 88 | }) 89 | 90 | t.Run("Raw", func(t *testing.T) { 91 | err := EnsureStackTrace(errors.New("achtung")) 92 | output := fmt.Sprintf("%+v", err) 93 | require.Contains(t, output, "achtung", output) 94 | require.NotContains(t, output, "EnsureStackTrace()", output) 95 | require.Contains(t, output, "TestStackTraceStart", output) 96 | }) 97 | 98 | } 99 | 100 | func TestStackTraceEnhance(t *testing.T) { 101 | err := stackTestStart() 102 | output := fmt.Sprintf("%+v", err) 103 | 104 | expected := map[string]int{ 105 | "TestStackTraceEnhance()": 0, 106 | "stackTestStart()": 0, 107 | "stackTestWithChan()": 0, 108 | "stackTest2()": 0, 109 | } 110 | checkStackTrace(t, output, expected) 111 | } 112 | 113 | func stackTestStart() error { 114 | ch := make(chan error) 115 | go stackTestWithChan(ch) 116 | return EnhanceStackTrace(<-ch, "") 117 | } 118 | 119 | func stackTestWithChan(ch chan error) { 120 | err := stackTest0() 121 | ch <- err 122 | } 123 | 124 | func stackTest0() error { 125 | return stackTest1() 126 | } 127 | 128 | func stackTest1() error { 129 | return stackTest2() 130 | } 131 | 132 | func stackTest2() error { 133 | return AssertionFailed.New("here be dragons") 134 | } 135 | 136 | func TestStackTraceDuplicate(t *testing.T) { 137 | err := stackTestDuplicate1() 138 | output := fmt.Sprintf("%+v", err) 139 | 140 | expected := map[string]int{ 141 | "TestStackTraceDuplicate()": 0, 142 | "stackTestDuplicate1()": 0, 143 | "stackTestStart1()": 0, 144 | "stackTestWithChan1()": 0, 145 | "stackTest21()": 0, 146 | } 147 | checkStackTrace(t, output, expected) 148 | } 149 | 150 | func stackTestDuplicate1() error { 151 | return EnhanceStackTrace(stackTestStart1(), "") 152 | } 153 | 154 | func stackTestStart1() error { 155 | ch := make(chan error) 156 | go stackTestWithChan1(ch) 157 | return EnhanceStackTrace(<-ch, "") 158 | } 159 | 160 | func stackTestWithChan1(ch chan error) { 161 | err := stackTest11() 162 | ch <- err 163 | } 164 | 165 | func stackTest11() error { 166 | return stackTest21() 167 | } 168 | 169 | func stackTest21() error { 170 | return AssertionFailed.New("here be dragons") 171 | } 172 | 173 | func TestStackTraceDuplicateWithIntermittentFrames(t *testing.T) { 174 | err := stackTestDuplicate2() 175 | output := fmt.Sprintf("%+v", err) 176 | 177 | expected := map[string]int{ 178 | "TestStackTraceDuplicateWithIntermittentFrames()": 0, 179 | "stackTestDuplicate2()": 0, 180 | "stackTestStart2()": 0, 181 | "enhanceFunc2()": 0, 182 | "stackTestWithChan2()": 0, 183 | "stackTest22()": 0, 184 | } 185 | checkStackTrace(t, output, expected) 186 | } 187 | 188 | func stackTestDuplicate2() error { 189 | err := stackTestStart2() 190 | return enhanceFunc2(err) 191 | } 192 | 193 | func enhanceFunc2(err error) error { 194 | return EnhanceStackTrace(err, "") 195 | } 196 | 197 | func stackTestStart2() error { 198 | ch := make(chan error) 199 | go stackTestWithChan2(ch) 200 | err := <-ch 201 | return EnhanceStackTrace(err, "") 202 | } 203 | 204 | func stackTestWithChan2(ch chan error) { 205 | err := stackTest12() 206 | ch <- err 207 | } 208 | 209 | func stackTest12() error { 210 | return stackTest22() 211 | } 212 | 213 | func stackTest22() error { 214 | return AssertionFailed.New("here be dragons and dungeons, too") 215 | } 216 | 217 | func checkStackTrace(t *testing.T, output string, expected map[string]int) { 218 | readByLine(t, output, func(line string) { 219 | for key := range expected { 220 | if strings.HasSuffix(line, key) { 221 | expected[key]++ 222 | } 223 | } 224 | }) 225 | 226 | for key, value := range expected { 227 | require.EqualValues(t, 1, value, "Wrong count (%d) of '%s' in:\n%s", value, key, output) 228 | } 229 | } 230 | 231 | func readByLine(t *testing.T, output string, f func(string)) { 232 | reader := bufio.NewReader(bytes.NewReader([]byte(output))) 233 | for { 234 | lineBytes, _, readErr := reader.ReadLine() 235 | if readErr == io.EOF { 236 | break 237 | } 238 | 239 | require.NoError(t, readErr) 240 | f(string(lineBytes)) 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /switch.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | // NotRecognisedType is a synthetic type used in TypeSwitch, signifying a presence of non-nil error of some other type. 4 | func NotRecognisedType() *Type { return notRecognisedType } 5 | 6 | // CaseNoError is a synthetic trait used in TraitSwitch, signifying an absence of error. 7 | func CaseNoError() Trait { return caseNoError } 8 | 9 | // CaseNoTrait is a synthetic trait used in TraitSwitch, signifying a presence of non-nil error that lacks specified traits. 10 | func CaseNoTrait() Trait { return caseNoTrait } 11 | 12 | var ( 13 | notRecognisedType = syntheticErrors.NewType("non.recognised") 14 | 15 | caseNoError = RegisterTrait("synthetic.no.error") 16 | caseNoTrait = RegisterTrait("synthetic.no.trait") 17 | ) 18 | 19 | // TypeSwitch is used to perform a switch around the type of an error. 20 | // For nil errors, returns nil. 21 | // For error types not in the 'types' list, including non-errorx errors, NotRecognisedType() is returned. 22 | // It is safe to treat NotRecognisedType() as 'any other type of not-nil error' case. 23 | // The effect is equivalent to a series of IsOfType() checks. 24 | // 25 | // NB: if more than one provided types matches the error, the first match in the providers list is recognised. 26 | func TypeSwitch(err error, types ...*Type) *Type { 27 | typed := Cast(err) 28 | 29 | switch { 30 | case err == nil: 31 | return nil 32 | case typed == nil: 33 | return NotRecognisedType() 34 | default: 35 | for _, t := range types { 36 | if typed.IsOfType(t) { 37 | return t 38 | } 39 | } 40 | 41 | return NotRecognisedType() 42 | } 43 | } 44 | 45 | // TraitSwitch is used to perform a switch around the trait of an error. 46 | // For nil errors, returns CaseNoError(). 47 | // For error types that lack any of the provided traits, including non-errorx errors, CaseNoTrait() is returned. 48 | // It is safe to treat CaseNoTrait() as 'any other kind of not-nil error' case. 49 | // The effect is equivalent to a series of HasTrait() checks. 50 | // 51 | // NB: if more than one provided types matches the error, the first match in the providers list is recognised. 52 | func TraitSwitch(err error, traits ...Trait) Trait { 53 | typed := Cast(err) 54 | 55 | switch { 56 | case err == nil: 57 | return CaseNoError() 58 | case typed == nil: 59 | return CaseNoTrait() 60 | default: 61 | for _, t := range traits { 62 | if typed.HasTrait(t) { 63 | return t 64 | } 65 | } 66 | 67 | return CaseNoTrait() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /switch_test.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestErrorSwitch(t *testing.T) { 11 | t.Run("Simple", func(t *testing.T) { 12 | switch testType.NewWithNoMessage().Type() { 13 | case testType: 14 | // OK 15 | default: 16 | require.Fail(t, "") 17 | } 18 | }) 19 | 20 | t.Run("Wrapped", func(t *testing.T) { 21 | err := testTypeBar1.Wrap(testType.NewWithNoMessage(), "a") 22 | require.Nil(t, Ignore(err, testTypeBar1)) 23 | require.NotNil(t, Ignore(err, testType)) 24 | 25 | switch TypeSwitch(err, testType, testTypeBar1) { 26 | case testType: 27 | require.Fail(t, "") 28 | case testTypeBar1: 29 | // OK 30 | case nil: 31 | require.Fail(t, "") 32 | default: 33 | require.Fail(t, "") 34 | } 35 | }) 36 | 37 | t.Run("Raw", func(t *testing.T) { 38 | switch TypeSwitch(fmt.Errorf("test non-errorx error"), testType, testTypeBar1) { 39 | case testType: 40 | require.Fail(t, "") 41 | case testTypeBar1: 42 | require.Fail(t, "") 43 | case nil: 44 | require.Fail(t, "") 45 | default: 46 | // OK 47 | } 48 | }) 49 | 50 | t.Run("Nil", func(t *testing.T) { 51 | switch TypeSwitch(nil, testType, testTypeBar1) { 52 | case testType: 53 | require.Fail(t, "") 54 | case testTypeBar1: 55 | require.Fail(t, "") 56 | case nil: 57 | // OK 58 | default: 59 | require.Fail(t, "") 60 | } 61 | }) 62 | 63 | t.Run("Supertype", func(t *testing.T) { 64 | switch TypeSwitch(Decorate(testSubtype0.New("b"), "c"), testType, testTypeBar1) { 65 | case testTypeBar1: 66 | require.Fail(t, "") 67 | case testType: 68 | // OK 69 | case nil: 70 | require.Fail(t, "") 71 | default: 72 | require.Fail(t, "") 73 | } 74 | }) 75 | 76 | t.Run("Subtype", func(t *testing.T) { 77 | switch TypeSwitch(Decorate(testSubtype0.New("b"), "c"), testSubtype0, testTypeBar1) { 78 | case testTypeBar1: 79 | require.Fail(t, "") 80 | case testType: 81 | require.Fail(t, "") 82 | case testSubtype0: 83 | // OK 84 | case nil: 85 | require.Fail(t, "") 86 | default: 87 | require.Fail(t, "") 88 | } 89 | }) 90 | 91 | t.Run("SubSubtype", func(t *testing.T) { 92 | switch TypeSwitch(Decorate(testSubtype0.New("b"), "c"), testSubtype1, testTypeBar1) { 93 | case testTypeBar1: 94 | require.Fail(t, "") 95 | case testType: 96 | require.Fail(t, "") 97 | case testSubtype0: 98 | require.Fail(t, "") 99 | case testSubtype1: 100 | require.Fail(t, "") 101 | case nil: 102 | require.Fail(t, "") 103 | default: 104 | // OK 105 | } 106 | }) 107 | 108 | t.Run("Ordering", func(t *testing.T) { 109 | switch TypeSwitch(Decorate(testSubtype0.New("b"), "c"), testSubtype1, testType, testSubtype0, testTypeBar1) { 110 | case testTypeBar1: 111 | require.Fail(t, "") 112 | case testType: 113 | // OK 114 | case testSubtype0: 115 | require.Fail(t, "") 116 | case testSubtype1: 117 | require.Fail(t, "") 118 | case nil: 119 | require.Fail(t, "") 120 | default: 121 | require.Fail(t, "") 122 | } 123 | }) 124 | } 125 | 126 | func TestErrorSwitchUnrecognised(t *testing.T) { 127 | t.Run("Mismatch", func(t *testing.T) { 128 | switch TypeSwitch(Decorate(testTypeBar2.New("b"), "c"), testTypeBar1) { 129 | case testTypeBar1: 130 | require.Fail(t, "") 131 | case nil: 132 | require.Fail(t, "") 133 | case NotRecognisedType(): 134 | // OK 135 | default: 136 | require.Fail(t, "") 137 | } 138 | }) 139 | 140 | t.Run("Raw", func(t *testing.T) { 141 | switch TypeSwitch(fmt.Errorf("test"), testTypeBar1) { 142 | case testType: 143 | require.Fail(t, "") 144 | case nil: 145 | require.Fail(t, "") 146 | case NotRecognisedType(): 147 | // OK 148 | default: 149 | require.Fail(t, "") 150 | } 151 | }) 152 | 153 | t.Run("Nil", func(t *testing.T) { 154 | switch TypeSwitch(nil, testTypeBar1) { 155 | case testType: 156 | require.Fail(t, "") 157 | case nil: 158 | // OK 159 | case NotRecognisedType(): 160 | require.Fail(t, "") 161 | default: 162 | require.Fail(t, "") 163 | } 164 | }) 165 | } 166 | 167 | func TestErrorTraitSwitch(t *testing.T) { 168 | err := traitTestTimeoutError.Wrap(traitTestError3.NewWithNoMessage(), "a") 169 | require.True(t, HasTrait(err, Timeout())) 170 | require.False(t, HasTrait(err, testTrait0)) 171 | 172 | t.Run("Wrapped", func(t *testing.T) { 173 | switch TraitSwitch(err, Timeout(), testTrait0) { 174 | case testTrait0: 175 | require.Fail(t, "") 176 | case Timeout(): 177 | // OK 178 | case CaseNoError(): 179 | require.Fail(t, "") 180 | case CaseNoTrait(): 181 | require.Fail(t, "") 182 | default: 183 | require.Fail(t, "") 184 | } 185 | }) 186 | 187 | t.Run("Raw", func(t *testing.T) { 188 | switch TraitSwitch(fmt.Errorf("test non-errorx error"), Timeout(), testTrait0) { 189 | case testTrait0: 190 | require.Fail(t, "") 191 | case Timeout(): 192 | require.Fail(t, "") 193 | case CaseNoError(): 194 | require.Fail(t, "") 195 | case CaseNoTrait(): 196 | // OK 197 | default: 198 | require.Fail(t, "") 199 | } 200 | }) 201 | 202 | t.Run("Nil", func(t *testing.T) { 203 | switch TraitSwitch(nil, Timeout(), testTrait0) { 204 | case testTrait0: 205 | require.Fail(t, "") 206 | case Timeout(): 207 | require.Fail(t, "") 208 | case CaseNoError(): 209 | // OK 210 | case CaseNoTrait(): 211 | require.Fail(t, "") 212 | default: 213 | require.Fail(t, "") 214 | } 215 | }) 216 | 217 | t.Run("NoMatch", func(t *testing.T) { 218 | switch TraitSwitch(err, testTrait0) { 219 | case testTrait0: 220 | require.Fail(t, "") 221 | case Timeout(): 222 | require.Fail(t, "") 223 | case CaseNoError(): 224 | require.Fail(t, "") 225 | case CaseNoTrait(): 226 | // OK 227 | default: 228 | require.Fail(t, "") 229 | } 230 | }) 231 | 232 | t.Run("Ordering", func(t *testing.T) { 233 | switch TraitSwitch(traitTestTemporaryTimeoutError.Wrap(traitTestError3.NewWithNoMessage(), "a"), Temporary(), Timeout()) { 234 | case Timeout(): 235 | require.Fail(t, "") 236 | case Temporary(): 237 | // OK 238 | case CaseNoError(): 239 | require.Fail(t, "") 240 | case CaseNoTrait(): 241 | require.Fail(t, "") 242 | default: 243 | require.Fail(t, "") 244 | } 245 | }) 246 | } 247 | -------------------------------------------------------------------------------- /trait.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | // Trait is a static characteristic of an error type. 4 | // All errors of a specific type possess exactly the same traits. 5 | // Traits are both defined along with an error and inherited from a supertype and a namespace. 6 | type Trait struct { 7 | id uint64 8 | label string 9 | } 10 | 11 | // RegisterTrait declares a new distinct traits. 12 | // Traits are matched exactly, distinct traits are considered separate event if they have the same label. 13 | func RegisterTrait(label string) Trait { 14 | return newTrait(label) 15 | } 16 | 17 | // HasTrait checks if an error possesses the expected trait. 18 | // Traits are always properties of a type rather than of an instance, so trait check is an alternative to a type check. 19 | // This alternative is preferable, though, as it is less brittle and generally creates less of a dependency. 20 | func HasTrait(err error, key Trait) bool { 21 | typedErr := Cast(err) 22 | if typedErr == nil { 23 | return false 24 | } 25 | 26 | return typedErr.HasTrait(key) 27 | } 28 | 29 | // Temporary is a trait that signifies that an error is temporary in nature. 30 | func Temporary() Trait { return traitTemporary } 31 | 32 | // Timeout is a trait that signifies that an error is some sort iof timeout. 33 | func Timeout() Trait { return traitTimeout } 34 | 35 | // NotFound is a trait that marks such an error where the requested object is not found. 36 | func NotFound() Trait { return traitNotFound } 37 | 38 | // Duplicate is a trait that marks such an error where an update is failed as a duplicate. 39 | func Duplicate() Trait { return traitDuplicate } 40 | 41 | // IsTemporary checks for Temporary trait. 42 | func IsTemporary(err error) bool { 43 | return HasTrait(err, Temporary()) 44 | } 45 | 46 | // IsTimeout checks for Timeout trait. 47 | func IsTimeout(err error) bool { 48 | return HasTrait(err, Timeout()) 49 | } 50 | 51 | // IsNotFound checks for NotFound trait. 52 | func IsNotFound(err error) bool { 53 | return HasTrait(err, NotFound()) 54 | } 55 | 56 | // IsDuplicate checks for Duplicate trait. 57 | func IsDuplicate(err error) bool { 58 | return HasTrait(err, Duplicate()) 59 | } 60 | 61 | var ( 62 | traitTemporary = RegisterTrait("temporary") 63 | traitTimeout = RegisterTrait("timeout") 64 | traitNotFound = RegisterTrait("not_found") 65 | traitDuplicate = RegisterTrait("duplicate") 66 | ) 67 | 68 | func newTrait(label string) Trait { 69 | return Trait{ 70 | id: nextInternalID(), 71 | label: label, 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /trait_test.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | var ( 10 | testTrait0 = RegisterTrait("test0") 11 | testTrait1 = RegisterTrait("test1") 12 | testTrait2 = RegisterTrait("test2") 13 | 14 | traitTestNamespace = NewNamespace("traits") 15 | traitTestNamespace2 = NewNamespace("traits2", testTrait0) 16 | traitTestNamespace2Child = traitTestNamespace2.NewSubNamespace("child", testTrait1) 17 | traitTestError = traitTestNamespace.NewType("simple", testTrait1) 18 | traitTestError2 = traitTestNamespace2.NewType("simple", testTrait2) 19 | traitTestError3 = traitTestNamespace2Child.NewType("simple", testTrait2) 20 | traitTestTimeoutError = traitTestNamespace.NewType("timeout", Timeout()) 21 | traitTestTemporaryTimeoutError = traitTestTimeoutError.NewSubtype("temporary", Temporary()) 22 | ) 23 | 24 | func TestTrait(t *testing.T) { 25 | t.Run("Negative", func(t *testing.T) { 26 | err := traitTestError.New("test") 27 | require.False(t, IsTemporary(err)) 28 | }) 29 | 30 | t.Run("Positive", func(t *testing.T) { 31 | err := traitTestError.New("test") 32 | require.True(t, HasTrait(err, testTrait1)) 33 | }) 34 | 35 | t.Run("SubType", func(t *testing.T) { 36 | err := traitTestTimeoutError.New("test") 37 | require.True(t, IsTimeout(err)) 38 | require.False(t, IsTemporary(err)) 39 | 40 | err = traitTestTemporaryTimeoutError.New("test") 41 | require.True(t, IsTimeout(err)) 42 | require.True(t, IsTemporary(err)) 43 | }) 44 | 45 | t.Run("Wrap", func(t *testing.T) { 46 | err := traitTestTimeoutError.New("test") 47 | err = traitTestError2.Wrap(err, "") 48 | 49 | require.False(t, IsTimeout(err)) 50 | require.True(t, HasTrait(err, testTrait0)) 51 | require.False(t, HasTrait(err, testTrait1)) 52 | require.True(t, HasTrait(err, testTrait2)) 53 | }) 54 | 55 | t.Run("Decorate", func(t *testing.T) { 56 | err := traitTestTimeoutError.New("test") 57 | err = Decorate(err, "") 58 | 59 | require.True(t, IsTimeout(err)) 60 | require.False(t, IsTemporary(err)) 61 | }) 62 | } 63 | 64 | func TestTraitNamespace(t *testing.T) { 65 | t.Run("Negative", func(t *testing.T) { 66 | err := traitTestError.New("test") 67 | require.False(t, HasTrait(err, testTrait0)) 68 | require.True(t, HasTrait(err, testTrait1)) 69 | require.False(t, HasTrait(err, testTrait2)) 70 | }) 71 | 72 | t.Run("Inheritance", func(t *testing.T) { 73 | err := traitTestError2.New("test") 74 | require.True(t, HasTrait(err, testTrait0)) 75 | require.False(t, HasTrait(err, testTrait1)) 76 | require.True(t, HasTrait(err, testTrait2)) 77 | }) 78 | 79 | t.Run("DoubleInheritance", func(t *testing.T) { 80 | err := traitTestError3.New("test") 81 | require.True(t, HasTrait(err, testTrait0)) 82 | require.True(t, HasTrait(err, testTrait1)) 83 | require.True(t, HasTrait(err, testTrait2)) 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /type.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "encoding" 5 | ) 6 | 7 | // Type is a distinct error type. 8 | // Belongs to a namespace, may be a descendant of another type in the same namespace. 9 | // May contain or inherit modifiers that alter the default properties for any error of this type. 10 | // May contain or inherit traits that all errors of this type will possess. 11 | type Type struct { 12 | namespace Namespace 13 | parent *Type 14 | id uint64 15 | fullName string 16 | traits map[Trait]bool 17 | modifiers modifiers 18 | } 19 | 20 | var _ encoding.TextMarshaler = (*Type)(nil) 21 | 22 | // NewType defines a new distinct type within a namespace. 23 | func NewType(namespace Namespace, name string, traits ...Trait) *Type { 24 | return newType(namespace, nil, name, traits...) 25 | } 26 | 27 | // NewSubtype defines a new subtype within a namespace of a parent type. 28 | func (t *Type) NewSubtype(name string, traits ...Trait) *Type { 29 | return newType(t.namespace, t, name, traits...) 30 | } 31 | 32 | // ApplyModifiers makes a one-time modification of defaults in error creation. 33 | func (t *Type) ApplyModifiers(modifiers ...TypeModifier) *Type { 34 | t.modifiers = t.modifiers.ReplaceWith(newTypeModifiers(modifiers...)) 35 | return t 36 | } 37 | 38 | // New creates an error of this type with a message. 39 | // Without args, leaves the original message intact, so a message may be generated or provided externally. 40 | // With args, a formatting is performed, and it is therefore expected a format string to be constant. 41 | func (t *Type) New(message string, args ...interface{}) *Error { 42 | return NewErrorBuilder(t). 43 | WithConditionallyFormattedMessage(message, args...). 44 | Create() 45 | } 46 | 47 | // NewWithNoMessage creates an error of this type without any message. 48 | // May be used when other information is sufficient, such as error type and stack trace. 49 | func (t *Type) NewWithNoMessage() *Error { 50 | return NewErrorBuilder(t). 51 | Create() 52 | } 53 | 54 | // Wrap creates an error of this type with another as original cause. 55 | // As far as type checks are concerned, this error is the only one visible, with original present only in error message. 56 | // The original error will not pass its dynamic properties, and those are accessible only via direct walk over Cause() chain. 57 | // Without args, leaves the original message intact, so a message may be generated or provided externally. 58 | // With args, a formatting is performed, and it is therefore expected a format string to be constant. 59 | // NB: Wrap is NOT the reverse of errors.Unwrap() or Error.Unwrap() method; name may be changed in future releases to avoid confusion. 60 | func (t *Type) Wrap(err error, message string, args ...interface{}) *Error { 61 | return NewErrorBuilder(t). 62 | WithConditionallyFormattedMessage(message, args...). 63 | WithCause(err). 64 | Create() 65 | } 66 | 67 | // WrapWithNoMessage creates an error of this type with another as original cause and with no additional message. 68 | // May be used when other information is sufficient, such as error type, cause and its stack trace and message. 69 | // As far as type checks are concerned, this error is the only one visible, with original visible only in error message. 70 | // The original error will, however, pass its dynamic properties. 71 | func (t *Type) WrapWithNoMessage(err error) *Error { 72 | return NewErrorBuilder(t). 73 | WithCause(err). 74 | Create() 75 | } 76 | 77 | // IsOfType is a type check for error. 78 | // Returns true either if both are of exactly the same type, or if the same is true for one of current type's ancestors. 79 | func (t *Type) IsOfType(other *Type) bool { 80 | current := t 81 | for current != nil { 82 | if current.id == other.id { 83 | return true 84 | } 85 | 86 | current = current.parent 87 | } 88 | 89 | return false 90 | } 91 | 92 | // HasTrait checks if a type possesses the expected trait. 93 | func (t *Type) HasTrait(key Trait) bool { 94 | _, ok := t.traits[key] 95 | return ok 96 | } 97 | 98 | // IsOfType is a type check for errors. 99 | // Returns true either if both are of exactly the same type, or if the same is true for one of current type's ancestors. 100 | // Go 1.12 and below: for an error that does not have an errorx type, returns false. 101 | // Go 1.13 and above: for an error that does not have an errorx type, returns false unless it wraps another error of errorx type. 102 | func IsOfType(err error, t *Type) bool { 103 | return isOfType(err, t) 104 | } 105 | 106 | // Supertype returns a parent type, if present. 107 | func (t *Type) Supertype() *Type { 108 | return t.parent 109 | } 110 | 111 | // FullName returns a fully qualified name if type, is not presumed to be unique, see TypeSubscriber. 112 | func (t *Type) FullName() string { 113 | return t.fullName 114 | } 115 | 116 | // Namespace returns a namespace this type belongs to. 117 | func (t *Type) Namespace() Namespace { 118 | return t.namespace 119 | } 120 | 121 | // RootNamespace returns a base namespace this type belongs to. 122 | func (t *Type) RootNamespace() Namespace { 123 | n := t.namespace 124 | for n.parent != nil { 125 | n = *n.parent 126 | } 127 | return n 128 | } 129 | 130 | func (t *Type) String() string { 131 | return t.FullName() 132 | } 133 | 134 | // MarshalText implements encoding.TextMarshaler 135 | func (t *Type) MarshalText() (text []byte, err error) { 136 | return []byte(t.String()), nil 137 | } 138 | 139 | func newType(namespace Namespace, parent *Type, name string, traits ...Trait) *Type { 140 | collectModifiers := func() modifiers { 141 | if parent == nil { 142 | return newInheritedModifiers(namespace.modifiers) 143 | } 144 | return newInheritedModifiers(parent.modifiers) 145 | } 146 | 147 | collectTraits := func() map[Trait]bool { 148 | result := make(map[Trait]bool) 149 | if parent != nil { 150 | for trait := range parent.traits { 151 | result[trait] = true 152 | } 153 | } 154 | 155 | for trait := range namespace.collectTraits() { 156 | result[trait] = true 157 | } 158 | 159 | for _, trait := range traits { 160 | result[trait] = true 161 | } 162 | 163 | return result 164 | } 165 | 166 | createFullName := func() string { 167 | if parent == nil { 168 | return namespace.FullName() + "." + name 169 | } 170 | return parent.FullName() + "." + name 171 | } 172 | 173 | t := &Type{ 174 | id: nextInternalID(), 175 | namespace: namespace, 176 | parent: parent, 177 | fullName: createFullName(), 178 | traits: collectTraits(), 179 | modifiers: collectModifiers(), 180 | } 181 | 182 | globalRegistry.registerType(t) 183 | return t 184 | } 185 | -------------------------------------------------------------------------------- /type_test.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestTypeName(t *testing.T) { 11 | require.Equal(t, "foo.bar", testType.FullName()) 12 | } 13 | 14 | func TestSubTypeName(t *testing.T) { 15 | require.Equal(t, "foo.bar.internal.wat", testSubtype1.FullName()) 16 | } 17 | 18 | func TestErrorTypeCheck(t *testing.T) { 19 | require.True(t, testSubtype1.IsOfType(testSubtype1)) 20 | require.False(t, testSubtype1.IsOfType(NewNamespace("a").NewType("b"))) 21 | } 22 | 23 | func TestErrorTypeCheckNonErrorx(t *testing.T) { 24 | require.False(t, IsOfType(errors.New("test"), testSubtype1)) 25 | } 26 | 27 | func TestErrorTypeUpCast(t *testing.T) { 28 | require.True(t, testSubtype1.IsOfType(testSubtype0)) 29 | require.True(t, testSubtype1.IsOfType(testType)) 30 | } 31 | 32 | func TestErrorTypeDownCast(t *testing.T) { 33 | require.False(t, testSubtype0.IsOfType(testSubtype1)) 34 | require.False(t, testType.IsOfType(testSubtype1)) 35 | } 36 | 37 | func TestErrorTypeSiblingsCast(t *testing.T) { 38 | subtype10 := testSubtype0.NewSubtype("wat!") 39 | subtype11 := testSubtype0.NewSubtype("oops") 40 | 41 | require.False(t, subtype10.IsOfType(subtype11)) 42 | require.False(t, subtype11.IsOfType(subtype10)) 43 | } 44 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | // Cast attempts to cast an error to errorx Type, returns nil if cast has failed. 4 | func Cast(err error) *Error { 5 | if e, ok := err.(*Error); ok && e != nil { 6 | return e 7 | } 8 | 9 | return nil 10 | } 11 | 12 | // Ignore returns nil if an error is of one of the provided types, returns the provided error otherwise. 13 | // May be used if a particular error signifies a mark in control flow rather than an error to be reported to the caller. 14 | func Ignore(err error, types ...*Type) error { 15 | if e := Cast(err); e != nil { 16 | for _, t := range types { 17 | if e.IsOfType(t) { 18 | return nil 19 | } 20 | } 21 | } 22 | 23 | return err 24 | } 25 | 26 | // IgnoreWithTrait returns nil if an error has one of the provided traits, returns the provided error otherwise. 27 | // May be used if a particular error trait signifies a mark in control flow rather than an error to be reported to the caller. 28 | func IgnoreWithTrait(err error, traits ...Trait) error { 29 | if e := Cast(err); e != nil { 30 | for _, t := range traits { 31 | if e.HasTrait(t) { 32 | return nil 33 | } 34 | } 35 | } 36 | 37 | return err 38 | } 39 | 40 | // GetTypeName returns the full type name if an error; returns an empty string for non-errorx error. 41 | // For decorated errors, the type of an original cause is used. 42 | func GetTypeName(err error) string { 43 | if e := Cast(err); e != nil { 44 | t := e.Type() 45 | if t != foreignType { 46 | return t.FullName() 47 | } 48 | } 49 | 50 | return "" 51 | } 52 | 53 | // ReplicateError is a utility function to duplicate error N times. 54 | // May be handy do demultiplex a single original error to a number of callers/requests. 55 | func ReplicateError(err error, count int) []error { 56 | result := make([]error, count) 57 | for i := range result { 58 | result[i] = err 59 | } 60 | return result 61 | } 62 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestIgnoreWithTrait(t *testing.T) { 11 | t.Run("Empty", func(t *testing.T) { 12 | require.Error(t, IgnoreWithTrait(TimeoutElapsed.NewWithNoMessage())) 13 | }) 14 | 15 | t.Run("AnotherTrait", func(t *testing.T) { 16 | require.Error(t, IgnoreWithTrait(TimeoutElapsed.NewWithNoMessage(), NotFound())) 17 | }) 18 | 19 | t.Run("Positive", func(t *testing.T) { 20 | require.NoError(t, IgnoreWithTrait(TimeoutElapsed.NewWithNoMessage(), Timeout())) 21 | }) 22 | 23 | t.Run("OneOfMany", func(t *testing.T) { 24 | require.NoError(t, IgnoreWithTrait(TimeoutElapsed.NewWithNoMessage(), NotFound(), Timeout())) 25 | }) 26 | } 27 | 28 | func TestGetTypeName(t *testing.T) { 29 | t.Run("Simple", func(t *testing.T) { 30 | require.EqualValues(t, "common.assertion_failed", GetTypeName(AssertionFailed.NewWithNoMessage())) 31 | }) 32 | 33 | t.Run("Wrap", func(t *testing.T) { 34 | require.EqualValues(t, "common.illegal_state", GetTypeName(IllegalState.WrapWithNoMessage(AssertionFailed.NewWithNoMessage()))) 35 | }) 36 | 37 | t.Run("Decorate", func(t *testing.T) { 38 | require.EqualValues(t, "common.assertion_failed", GetTypeName(Decorate(AssertionFailed.NewWithNoMessage(), ""))) 39 | }) 40 | 41 | t.Run("Nil", func(t *testing.T) { 42 | require.EqualValues(t, "", GetTypeName(nil)) 43 | }) 44 | 45 | t.Run("Raw", func(t *testing.T) { 46 | require.EqualValues(t, "", GetTypeName(errors.New("test"))) 47 | }) 48 | 49 | t.Run("DecoratedRaw", func(t *testing.T) { 50 | require.EqualValues(t, "", GetTypeName(Decorate(errors.New("test"), ""))) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /wrap.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | var ( 4 | // Most errors from this namespace are made private in order to disallow and direct type checks in the user code 5 | syntheticErrors = NewNamespace("synthetic") 6 | 7 | // Private error type for non-errors errors, used as a not-nil substitute that cannot be type-checked directly 8 | foreignType = syntheticErrors.NewType("foreign") 9 | // Private error type used as a universal wrapper, meant to add nothing at all to the error apart from some message 10 | transparentWrapper = syntheticErrors.NewType("decorate").ApplyModifiers(TypeModifierTransparent) 11 | // Private error type used as a densely opaque wrapper which hides both the original error and its own type 12 | opaqueWrapper = syntheticErrors.NewType("wrap") 13 | // Private error type used for stack trace capture 14 | stackTraceWrapper = syntheticErrors.NewType("stacktrace").ApplyModifiers(TypeModifierTransparent) 15 | ) 16 | 17 | // Decorate allows to pass some text info along with a message, leaving its semantics totally intact. 18 | // Perceived type, traits and properties of the resulting error are those of the original. 19 | // Without args, leaves the provided message intact, so a message may be generated or provided externally. 20 | // With args, a formatting is performed, and it is therefore expected a format string to be constant. 21 | func Decorate(err error, message string, args ...interface{}) *Error { 22 | return NewErrorBuilder(transparentWrapper). 23 | WithConditionallyFormattedMessage(message, args...). 24 | WithCause(err). 25 | Create() 26 | } 27 | 28 | // EnhanceStackTrace has all the properties of the Decorate() method 29 | // and additionally extends the stack trace of the original error. 30 | // Designed to be used when a original error is passed from another goroutine rather than from a direct method call. 31 | // If, however, it is called in the same goroutine, formatter makes some moderated effort to remove duplication. 32 | func EnhanceStackTrace(err error, message string, args ...interface{}) *Error { 33 | return NewErrorBuilder(transparentWrapper). 34 | WithConditionallyFormattedMessage(message, args...). 35 | WithCause(err). 36 | EnhanceStackTrace(). 37 | Create() 38 | } 39 | 40 | // EnsureStackTrace is a utility to ensure the stack trace is captured in provided error. 41 | // If this is already true, it is returned unmodified. 42 | // Otherwise, it is decorated with stack trace. 43 | func EnsureStackTrace(err error) *Error { 44 | if typedErr := Cast(err); typedErr != nil && typedErr.stackTrace != nil { 45 | return typedErr 46 | } 47 | 48 | return NewErrorBuilder(stackTraceWrapper). 49 | WithConditionallyFormattedMessage(""). 50 | WithCause(err). 51 | EnhanceStackTrace(). 52 | Create() 53 | } 54 | 55 | // DecorateMany performs a transparent wrap of multiple errors with additional message. 56 | // If there are no errors, or all errors are nil, returns nil. 57 | // If all errors are of the same type (for example, if there is only one), wraps them transparently. 58 | // Otherwise, an opaque wrap is performed, that is, IsOfType checks will fail on underlying error types. 59 | func DecorateMany(message string, errs ...error) error { 60 | errs = ignoreEmpty(errs) 61 | if len(errs) == 0 { 62 | return nil 63 | } 64 | 65 | if !areAllOfTheSameType(errs...) { 66 | return WrapMany(opaqueWrapper, message, errs...) 67 | } 68 | return WrapMany(transparentWrapper, message, errs...) 69 | } 70 | 71 | // WrapMany is a utility to wrap multiple errors. 72 | // If there are no errors, or all errors are nil, returns nil. 73 | // Otherwise, the fist error is treated as an original cause, others are added as underlying. 74 | func WrapMany(errorType *Type, message string, errs ...error) error { 75 | errs = ignoreEmpty(errs) 76 | if len(errs) == 0 { 77 | return nil 78 | } 79 | 80 | cause := errs[0] 81 | suppressed := errs[1:] 82 | return errorType.Wrap(cause, message).WithUnderlyingErrors(suppressed...) 83 | } 84 | 85 | func ignoreEmpty(errs []error) []error { 86 | result := make([]error, 0, len(errs)) 87 | for _, err := range errs { 88 | if err != nil { 89 | result = append(result, err) 90 | } 91 | } 92 | return result 93 | } 94 | 95 | func areAllOfTheSameType(errs ...error) bool { 96 | if len(errs) < 2 { 97 | return true 98 | } 99 | 100 | var errorType *Type 101 | for _, err := range errs { 102 | typedError := Cast(err) 103 | if typedError == nil { 104 | return false 105 | } 106 | 107 | if errorType == nil { 108 | errorType = typedError.Type() 109 | } else if errorType != typedError.Type() { 110 | return false 111 | } 112 | } 113 | 114 | return true 115 | } 116 | -------------------------------------------------------------------------------- /wrap_test.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestEnsureStackTrace(t *testing.T) { 12 | t.Run("Simple", func(t *testing.T) { 13 | err := EnsureStackTrace(testType.New("good")) 14 | require.True(t, IsOfType(err, testType)) 15 | output := fmt.Sprintf("%+v", err) 16 | require.Contains(t, output, "good", output) 17 | require.Contains(t, output, "TestEnsureStackTrace", output) 18 | }) 19 | 20 | t.Run("NoTrace", func(t *testing.T) { 21 | err := EnsureStackTrace(testTypeSilent.New("average")) 22 | require.True(t, IsOfType(err, testType)) 23 | output := fmt.Sprintf("%+v", err) 24 | require.Contains(t, output, "average", output) 25 | require.Contains(t, output, "TestEnsureStackTrace", output) 26 | }) 27 | 28 | t.Run("Raw", func(t *testing.T) { 29 | err := EnsureStackTrace(errors.New("bad")) 30 | output := fmt.Sprintf("%+v", err) 31 | require.Contains(t, output, "bad", output) 32 | require.Contains(t, output, "TestEnsureStackTrace", output) 33 | }) 34 | } 35 | 36 | func TestDecorateMany(t *testing.T) { 37 | t.Run("Single", func(t *testing.T) { 38 | err := DecorateMany("ouch!", testType.NewWithNoMessage()) 39 | require.Equal(t, "ouch!, cause: foo.bar", err.Error()) 40 | require.True(t, IsOfType(err, testType)) 41 | require.Equal(t, testType, err.(*Error).Type()) 42 | }) 43 | 44 | t.Run("SingleEmpty", func(t *testing.T) { 45 | require.Nil(t, DecorateMany("ouch!", nil)) 46 | }) 47 | 48 | t.Run("ManyEmpty", func(t *testing.T) { 49 | require.Nil(t, DecorateMany("ouch!", nil, nil)) 50 | require.Nil(t, DecorateMany("ouch!", nil, nil, nil)) 51 | }) 52 | 53 | t.Run("ManySame", func(t *testing.T) { 54 | err := DecorateMany("ouch!", testType.NewWithNoMessage(), nil, testType.New("bad")) 55 | require.Equal(t, "ouch!, cause: foo.bar (hidden: foo.bar: bad)", err.Error()) 56 | require.True(t, IsOfType(err, testType)) 57 | require.Equal(t, testType, err.(*Error).Type()) 58 | }) 59 | 60 | t.Run("ManyDifferent", func(t *testing.T) { 61 | err := DecorateMany("ouch!", testTypeBar1.NewWithNoMessage(), testTypeBar2.New("bad"), nil) 62 | require.Equal(t, "synthetic.wrap: ouch!, cause: foo.bar1 (hidden: foo.bar2: bad)", err.Error()) 63 | require.False(t, IsOfType(err, testTypeBar1)) 64 | require.False(t, IsOfType(err, testTypeBar2)) 65 | require.NotEqual(t, testTypeBar1, err.(*Error).Type()) 66 | require.NotEqual(t, testTypeBar2, err.(*Error).Type()) 67 | }) 68 | } 69 | --------------------------------------------------------------------------------