pax_global_header00006660000000000000000000000064147134037230014516gustar00rootroot0000000000000052 comment=3280086cb5400e2900300ba010ddf658fa717fc4 errorx-1.2.0/000077500000000000000000000000001471340372300130375ustar00rootroot00000000000000errorx-1.2.0/.github/000077500000000000000000000000001471340372300143775ustar00rootroot00000000000000errorx-1.2.0/.github/workflows/000077500000000000000000000000001471340372300164345ustar00rootroot00000000000000errorx-1.2.0/.github/workflows/ci.yml000066400000000000000000000006461471340372300175600ustar00rootroot00000000000000name: CI on: [push] jobs: build: runs-on: ubuntu-latest strategy: matrix: 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] steps: - name: Checkout uses: actions/checkout@v2 - name: Set up Go ${{ matrix.go }} uses: actions/setup-go@v1 with: go-version: ${{ matrix.go }} - name: Build run: go test -v ./... errorx-1.2.0/.gitignore000066400000000000000000000003241471340372300150260ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # IDE .idea/ *.iml errorx-1.2.0/.travis.yml000066400000000000000000000004331471340372300151500ustar00rootroot00000000000000language: go before_script: if [[ $TRAVIS_GO_VERSION =~ (^1\.8) ]]; then cd $GOPATH/src/github.com/stretchr/testify/ && git checkout v1.2.2 && cd -; fi go: - "1.8.x" - "1.11.x" - "1.12.x" - "1.13.x" - "1.14.x" - "1.15.x" - master env: global: - GO111MODULE=on errorx-1.2.0/LICENSE000066400000000000000000000020451471340372300140450ustar00rootroot00000000000000MIT License Copyright (c) 2018 Joom Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. errorx-1.2.0/README.md000066400000000000000000000242371471340372300143260ustar00rootroot00000000000000[![Github Actions Build Status](https://github.com/joomcode/errorx/workflows/CI/badge.svg)](https://github.com/joomcode/errorx/actions) [![GoDoc](https://img.shields.io/static/v1?label=godev&message=reference&color=00add8)](https://pkg.go.dev/github.com/joomcode/errorx?tab=doc) [![Report Card](https://goreportcard.com/badge/github.com/joomcode/errorx)](https://goreportcard.com/report/github.com/joomcode/errorx) [![gocover.io](https://gocover.io/_badge/github.com/joomcode/errorx)](https://gocover.io/github.com/joomcode/errorx) [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go#error-handling) ## Highlights The *errorx* library provides error implementation and error-related utilities. Library features include (but are not limited to): * Stack traces * Composability of errors * Means to enhance error both with stack trace and with message * Robust type and trait checks ## Introduction Conventional approach towards errors in *Go* is quite limited. The typical case implies an error being created at some point: ```go return errors.New("now this is unfortunate") ``` Then being passed along with a no-brainer: ```go if err != nil { return err } ``` And, finally, handled by printing it to the log file: ```go log.Printf("Error: %s", err) ``` 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: > Error: EOF > Error: unexpected '>' at the beginning of value > Error: wrong argument value An *errorx* library makes an approach to create a toolset that would help remedy this issue with these considerations in mind: * 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 * 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 * Errors must be composable, and patterns like ```if err == io.EOF``` defeat that purpose, so they should be avoided * 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 * It must be easy to create an error, add some context to it, check for it * 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 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. 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. # errorx With *errorx*, the pattern above looks like this: ```go return errorx.IllegalState.New("unfortunate") ``` ```go if err != nil { return errorx.Decorate(err, "this could be so much better") } ``` ```go log.Printf("Error: %+v", err) ``` An error message will look something like this: ``` Error: this could be so much better, cause: common.illegal_state: unfortunate at main.culprit() main.go:21 at main.innocent() main.go:16 at main.main() main.go:11 ``` 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. 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. ## Error check If an error requires special treatment, it may be done like this: ```go // MyError = MyErrors.NewType("my_error") if errorx.IsOfType(err, MyError) { // handle } ``` 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. An alternative is a mechanisms called **traits**: ```go // the first parameter is a name of new error type, the second is a reference to existing trait TimeoutElapsed = MyErrors.NewType("timeout", errorx.Timeout()) ``` Here, ```TimeoutElapsed``` error type is created with a Timeout() trait, and errors may be checked against it: ```go if errorx.HasTrait(err, errorx.Timeout()) { // handle } ``` 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. ## Wrap 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. 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. A better alternative is: ```go return MyError.Wrap(err, "fail") ``` With ```Wrap()```, an original error is fully retained for the log, but hidden from type checks by the caller. See ```WrapMany()``` and ```DecorateMany()``` for more sophisticated cases. ## Stack traces As an essential part of debug information, stack traces are included in all *errorx* errors by default. 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: ```go return errorx.EnhanceStackTrace(<-errorChan, "task failed") ``` Result would look like this: ``` Error: task failed, cause: common.illegal_state: unfortunate at main.proxy() main.go:17 at main.main() main.go:11 ---------------------------------- at main.culprit() main.go:26 at main.innocent() main.go:21 ``` 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: ```go ErrInvalidToken = AuthErrors.NewType("invalid_token").ApplyModifiers(errorx.TypeModifierOmitStackTrace) ``` 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. Other relevant tools include ```EnsureStackTrace(err)``` to provide an error of unknown nature with a stack trace, if it lacks one. ### Stack traces benchmark 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. Result sample, MacBook Pro Intel Core i7-6920HQ CPU @ 2.90GHz 4 core: name | runs | ns/op | note ------ | ------: | ------: | ------ BenchmarkSimpleError10 | 20000000 | 57.2 | simple error, 10 frames deep BenchmarkErrorxError10 | 10000000 | 138 | same with errorx error BenchmarkStackTraceErrorxError10 | 1000000 | 1601 | same with collected stack trace BenchmarkSimpleError100 | 3000000 | 421 | simple error, 100 frames deep BenchmarkErrorxError100 | 3000000 | 507 | same with errorx error BenchmarkStackTraceErrorxError100 | 300000 | 4450 | same with collected stack trace BenchmarkStackTraceNaiveError100-8 | 2000 | 588135 | same with naive debug.Stack() error implementation BenchmarkSimpleErrorPrint100 | 2000000 | 617 | simple error, 100 frames deep, format output BenchmarkErrorxErrorPrint100 | 2000000 | 935 | same with errorx error BenchmarkStackTraceErrorxErrorPrint100 | 30000 | 58965 | same with collected stack trace BenchmarkStackTraceNaiveErrorPrint100-8 | 2000 | 599155 | same with naive debug.Stack() error implementation Key takeaways: * With deep enough call stack, trace capture brings **10x slowdown** * This is an absolute **worst case measurement, no-op function**; in a real life, much more time is spent doing actual work * Then again, in real life code invocation does not always result in error, so the overhead is proportional to the % of error returns * Still, it pays to omit stack trace collection when it would be of no use * It is actually **much more expensive to format** an error with a stack trace than to create it, roughly **another 10x** * Compared to the most naive approach to stack trace collection, error creation it is **100x** cheaper with errorx * Therefore, it is totally OK to create an error with a stack trace that would then be handled and not printed to log * 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 ## More See [godoc](https://godoc.org/github.com/joomcode/errorx) for other *errorx* features: * Namespaces * Type switches * ```errorx.Ignore``` * Trait inheritance * Dynamic properties * Panic-related utils * Type registry * etc. errorx-1.2.0/benchmark/000077500000000000000000000000001471340372300147715ustar00rootroot00000000000000errorx-1.2.0/benchmark/stacktrace_benchmark_test.go000066400000000000000000000102101471340372300225070ustar00rootroot00000000000000package benchmark import ( "errors" "fmt" "runtime/debug" "testing" "github.com/joomcode/errorx" ) var errorSink error func BenchmarkSimpleError10(b *testing.B) { for n := 0; n < b.N; n++ { errorSink = function0(10, createSimpleError) } consumeResult(errorSink) } func BenchmarkErrorxError10(b *testing.B) { for n := 0; n < b.N; n++ { errorSink = function0(10, createSimpleErrorxError) } consumeResult(errorSink) } func BenchmarkStackTraceErrorxError10(b *testing.B) { for n := 0; n < b.N; n++ { errorSink = function0(10, createErrorxError) } consumeResult(errorSink) } func BenchmarkSimpleError100(b *testing.B) { for n := 0; n < b.N; n++ { errorSink = function0(100, createSimpleError) } consumeResult(errorSink) } func BenchmarkErrorxError100(b *testing.B) { for n := 0; n < b.N; n++ { errorSink = function0(100, createSimpleErrorxError) } consumeResult(errorSink) } func BenchmarkStackTraceErrorxError100(b *testing.B) { for n := 0; n < b.N; n++ { errorSink = function0(100, createErrorxError) } consumeResult(errorSink) } func BenchmarkStackTraceNaiveError100(b *testing.B) { for n := 0; n < b.N; n++ { errorSink = function0(100, createNaiveError) } consumeResult(errorSink) } func BenchmarkSimpleErrorPrint100(b *testing.B) { for n := 0; n < b.N; n++ { err := function0(100, createSimpleError) emulateErrorPrint(err) errorSink = err } consumeResult(errorSink) } func BenchmarkErrorxErrorPrint100(b *testing.B) { for n := 0; n < b.N; n++ { err := function0(100, createSimpleErrorxError) emulateErrorPrint(err) errorSink = err } consumeResult(errorSink) } func BenchmarkStackTraceErrorxErrorPrint100(b *testing.B) { for n := 0; n < b.N; n++ { err := function0(100, createErrorxError) emulateErrorPrint(err) errorSink = err } consumeResult(errorSink) } func BenchmarkStackTraceNaiveErrorPrint100(b *testing.B) { for n := 0; n < b.N; n++ { err := function0(100, createNaiveError) emulateErrorPrint(err) errorSink = err } consumeResult(errorSink) } func createSimpleError() error { return errors.New("benchmark") } var ( Errors = errorx.NewNamespace("errorx.benchmark") NoStackTraceError = Errors.NewType("no_stack_trace").ApplyModifiers(errorx.TypeModifierOmitStackTrace) StackTraceError = Errors.NewType("stack_trace") ) func createSimpleErrorxError() error { return NoStackTraceError.New("benchmark") } func createErrorxError() error { return StackTraceError.New("benchmark") } type naiveError struct { stack []byte } func (err naiveError) Error() string { return fmt.Sprintf("benchmark\n%s", err.stack) } func createNaiveError() error { return naiveError{stack: debug.Stack()} } func function0(depth int, generate func() error) error { if depth == 0 { return generate() } switch depth % 3 { case 0: return function1(depth-1, generate) case 1: return function2(depth-1, generate) default: return function3(depth-1, generate) } } func function1(depth int, generate func() error) error { if depth == 0 { return generate() } return function4(depth-1, generate) } func function2(depth int, generate func() error) error { if depth == 0 { return generate() } return function4(depth-1, generate) } func function3(depth int, generate func() error) error { if depth == 0 { return generate() } return function4(depth-1, generate) } func function4(depth int, generate func() error) error { switch depth { case 0: return generate() default: return function0(depth-1, generate) } } type sinkError struct { value int } func (sinkError) Error() string { return "" } // Perform error formatting and consume the result to disallow optimizations against output func emulateErrorPrint(err error) { output := fmt.Sprintf("%+v", err) if len(output) > 10000 && output[1000:1004] == "DOOM" { panic("this was not supposed to happen") } } // Consume error with a possible side effect to disallow optimizations against err func consumeResult(err error) { if e, ok := err.(sinkError); ok && e.value == 1 { panic("this was not supposed to happen") } } // A public function to discourage optimizations against errorSink variable func ExportSink() error { return errorSink } errorx-1.2.0/builder.go000066400000000000000000000117151471340372300150210ustar00rootroot00000000000000package errorx import ( "fmt" "strconv" ) // ErrorBuilder is a utility to compose an error from type. // Typically, a direct usage is not required: either Type methods of helpers like Decorate are sufficient. // Only use builder if no simpler alternative is available. type ErrorBuilder struct { errorType *Type message string cause error mode callStackBuildMode isTransparent bool } // NewErrorBuilder creates error builder from an existing error type. func NewErrorBuilder(t *Type) ErrorBuilder { getMode := func() callStackBuildMode { if !t.modifiers.CollectStackTrace() { return stackTraceOmit } return stackTraceCollect } return ErrorBuilder{ errorType: t, mode: getMode(), isTransparent: t.modifiers.Transparent(), } } // WithCause provides an original cause for error. // For non-errorx errors, a stack trace is collected unless Type tells otherwise. // Otherwise, it is inherited by default, as error wrapping is typically performed 'en passe'. // Note that even if an original error explicitly omitted the stack trace, it could be added on wrap. func (eb ErrorBuilder) WithCause(err error) ErrorBuilder { eb.cause = err if Cast(err) != nil { if eb.errorType.modifiers.CollectStackTrace() { eb.mode = stackTraceBorrowOrCollect } else { eb.mode = stackTraceBorrowOnly } } return eb } // Transparent makes a wrap transparent rather than opaque (default). // Transparent wrap hides the current error type from the type checks and exposes the error type of the cause instead. // The same holds true for traits, and the dynamic properties are visible from both cause and transparent wrapper. // Note that if the cause error is non-errorx, transparency will still hold, type check against wrapper will still fail. func (eb ErrorBuilder) Transparent() ErrorBuilder { if eb.cause == nil { panic("wrong builder usage: wrap modifier without non-nil cause") } eb.isTransparent = true return eb } // EnhanceStackTrace is a signal to collect the current stack trace along with the original one, and use both in formatting. // If the original error does not hold a stack trace for whatever reason, it will be collected it this point. // This is typically a way to handle an error received from another goroutine - say, a worker pool. // When stack traces overlap, formatting makes a conservative attempt not to repeat itself, // preserving the *original* stack trace in its entirety. func (eb ErrorBuilder) EnhanceStackTrace() ErrorBuilder { if eb.cause == nil { panic("wrong builder usage: wrap modifier without non-nil cause") } if Cast(eb.cause) != nil { eb.mode = stackTraceEnhance } else { eb.mode = stackTraceCollect } return eb } // WithConditionallyFormattedMessage provides a message for an error in flexible format, to simplify its usages. // Without args, leaves the original message intact, so a message may be generated or provided externally. // With args, a formatting is performed, and it is therefore expected a format string to be constant. func (eb ErrorBuilder) WithConditionallyFormattedMessage(message string, args ...interface{}) ErrorBuilder { if len(args) == 0 { eb.message = message } else { eb.message = fmt.Sprintf(message, args...) } return eb } // Create returns an error with specified params. func (eb ErrorBuilder) Create() *Error { err := &Error{ errorType: eb.errorType, message: eb.message, cause: eb.cause, transparent: eb.isTransparent, stackTrace: eb.assembleStackTrace(), } return err } type callStackBuildMode int const ( stackTraceCollect callStackBuildMode = 1 stackTraceBorrowOrCollect callStackBuildMode = 2 stackTraceBorrowOnly callStackBuildMode = 3 stackTraceEnhance callStackBuildMode = 4 stackTraceOmit callStackBuildMode = 5 ) func (eb ErrorBuilder) assembleStackTrace() *stackTrace { switch eb.mode { case stackTraceCollect: return eb.collectOriginalStackTrace() case stackTraceBorrowOnly: return eb.borrowStackTraceFromCause() case stackTraceBorrowOrCollect: if st := eb.borrowStackTraceFromCause(); st != nil { return st } return eb.collectOriginalStackTrace() case stackTraceEnhance: return eb.combineStackTraceWithCause() case stackTraceOmit: return nil default: panic("unknown mode " + strconv.Itoa(int(eb.mode))) } } func (eb ErrorBuilder) collectOriginalStackTrace() *stackTrace { return collectStackTrace() } func (eb ErrorBuilder) borrowStackTraceFromCause() *stackTrace { return eb.extractStackTraceFromCause(eb.cause) } func (eb ErrorBuilder) combineStackTraceWithCause() *stackTrace { currentStackTrace := collectStackTrace() originalStackTrace := eb.extractStackTraceFromCause(eb.cause) if originalStackTrace != nil { currentStackTrace.enhanceWithCause(originalStackTrace) } return currentStackTrace } func (eb ErrorBuilder) extractStackTraceFromCause(cause error) *stackTrace { if typedCause := Cast(cause); typedCause != nil { return typedCause.stackTrace } return nil } errorx-1.2.0/builder_test.go000066400000000000000000000031041471340372300160510ustar00rootroot00000000000000package errorx import ( "errors" "fmt" "testing" "github.com/stretchr/testify/require" ) func TestBuilderTransparency(t *testing.T) { t.Run("Raw", func(t *testing.T) { err := NewErrorBuilder(testType).WithCause(errors.New("bad thing")).Transparent().Create() require.False(t, err.IsOfType(testType)) require.NotEqual(t, testType, err.Type()) }) t.Run("RawWithModifier", func(t *testing.T) { err := NewErrorBuilder(testTypeTransparent).WithCause(errors.New("bad thing")).Create() require.False(t, err.IsOfType(testType)) require.NotEqual(t, testType, err.Type()) }) } func testBuilderRespectsNoStackTraceMarkerFrame() error { return testType.NewWithNoMessage() } func TestBuilderRespectsNoStackTrace(t *testing.T) { wrapperErrorTypes := []*Type{testTypeSilent, testTypeSilentTransparent} for _, et := range wrapperErrorTypes { t.Run(et.String(), func(t *testing.T) { t.Run("Naked", func(t *testing.T) { err := NewErrorBuilder(et). WithCause(errors.New("naked error")). Create() require.Nil(t, err.stackTrace) }) t.Run("WithoutStacktrace", func(t *testing.T) { err := NewErrorBuilder(et). WithCause(testTypeSilent.NewWithNoMessage()). Create() require.Nil(t, err.stackTrace) }) t.Run("WithStacktrace", func(t *testing.T) { cause := testBuilderRespectsNoStackTraceMarkerFrame() err := NewErrorBuilder(et). WithCause(cause). Create() require.Same(t, err.stackTrace, Cast(cause).stackTrace) require.Contains(t, fmt.Sprintf("%+v", err), "testBuilderRespectsNoStackTraceMarkerFrame") }) }) } } errorx-1.2.0/common.go000066400000000000000000000040601471340372300146560ustar00rootroot00000000000000package errorx var ( // CommonErrors is a namespace for general purpose errors designed for universal use. // These errors should typically be used in opaque manner, implying no handing in user code. // When handling is required, it is best to use custom error types with both standard and custom traits. CommonErrors = NewNamespace("common") // IllegalArgument is a type for invalid argument error IllegalArgument = CommonErrors.NewType("illegal_argument") // IllegalState is a type for invalid state error IllegalState = CommonErrors.NewType("illegal_state") // IllegalFormat is a type for invalid format error IllegalFormat = CommonErrors.NewType("illegal_format") // InitializationFailed is a type for initialization error InitializationFailed = CommonErrors.NewType("initialization_failed") // DataUnavailable is a type for unavailable data error DataUnavailable = CommonErrors.NewType("data_unavailable") // UnsupportedOperation is a type for unsupported operation error UnsupportedOperation = CommonErrors.NewType("unsupported_operation") // RejectedOperation is a type for rejected operation error RejectedOperation = CommonErrors.NewType("rejected_operation") // Interrupted is a type for interruption error Interrupted = CommonErrors.NewType("interrupted") // AssertionFailed is a type for assertion error AssertionFailed = CommonErrors.NewType("assertion_failed") // InternalError is a type for internal error InternalError = CommonErrors.NewType("internal_error") // ExternalError is a type for external error ExternalError = CommonErrors.NewType("external_error") // ConcurrentUpdate is a type for concurrent update error ConcurrentUpdate = CommonErrors.NewType("concurrent_update") // TimeoutElapsed is a type for timeout error TimeoutElapsed = CommonErrors.NewType("timeout", Timeout()) // NotImplemented is an error type for lacking implementation NotImplemented = UnsupportedOperation.NewSubtype("not_implemented") // UnsupportedVersion is a type for unsupported version error UnsupportedVersion = UnsupportedOperation.NewSubtype("version") ) errorx-1.2.0/error.go000066400000000000000000000206631471340372300145260ustar00rootroot00000000000000package errorx import ( "fmt" "io" "strings" ) // Error is an instance of error object. // At the moment of creation, Error collects information based on context, creation modifiers and type it belongs to. // Error is mostly immutable, and distinct errors composition is achieved through wrap. type Error struct { message string errorType *Type cause error stackTrace *stackTrace // properties are used both for public properties inherited through "transparent" wrapping // and for some optional per-instance information like "underlying errors" properties *propertyMap transparent bool hasUnderlying bool printablePropertyCount uint8 } var _ fmt.Formatter = (*Error)(nil) // WithProperty adds a dynamic property to error instance. // If an error already contained another value for the same property, it is overwritten. // It is a caller's responsibility to accumulate and update a property, if needed. // Dynamic properties is a brittle mechanism and should therefore be used with care and in a simple and robust manner. // 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. func (e *Error) WithProperty(key Property, value interface{}) *Error { errorCopy := *e errorCopy.properties = errorCopy.properties.with(key, value) if key.printable && errorCopy.printablePropertyCount < 255 { errorCopy.printablePropertyCount++ } return &errorCopy } // WithUnderlyingErrors adds multiple additional related (hidden, suppressed) errors to be used exclusively in error output. // Note that these errors make no other effect whatsoever: their traits, types, properties etc. are lost on the observer. // Consider using errorx.DecorateMany instead. func (e *Error) WithUnderlyingErrors(errs ...error) *Error { underlying := e.underlying() newUnderlying := underlying for _, err := range errs { if err == nil { continue } newUnderlying = append(newUnderlying, err) } if len(newUnderlying) == len(underlying) { return e } l := len(newUnderlying) // note: l > 0, because non-increased 0 length is handled above errorCopy := e.WithProperty(propertyUnderlying, newUnderlying[:l:l]) errorCopy.hasUnderlying = true return errorCopy } // Property extracts a dynamic property value from an error. // A property may belong to this error or be extracted from the original cause. // The transparency rules are respected to some extent: both the original cause and the transparent wrapper // may have accessible properties, but an opaque wrapper hides the original properties. func (e *Error) Property(key Property) (interface{}, bool) { cause := e for cause != nil { value, ok := cause.properties.get(key) if ok { return value, true } if !cause.transparent { break } cause = Cast(cause.Cause()) } return nil, false } // HasTrait checks if an error possesses the expected trait. // Trait check works just as a type check would: opaque wrap hides the traits of the cause. // Traits are always properties of a type rather than of an instance, so trait check is an alternative to a type check. // This alternative is preferable, though, as it is less brittle and generally creates less of a dependency. func (e *Error) HasTrait(key Trait) bool { cause := e for cause != nil { if !cause.transparent { return cause.errorType.HasTrait(key) } cause = Cast(cause.Cause()) } return false } // IsOfType is a proper type check for an errorx-based errors. // It takes the transparency and error types hierarchy into account, // so that type check against any supertype of the original cause passes. // Go 1.13 and above: it also tolerates non-errorx errors in chain if those errors support errors unwrap. func (e *Error) IsOfType(t *Type) bool { return e.isOfType(t) } // Type returns the exact type of this error. // With transparent wrapping, such as in Decorate(), returns the type of the original cause. // The result is always not nil, even if the resulting type is impossible to successfully type check against. // // NB: the exact error type may fail an equality check where a IsOfType() check would succeed. // This may happen if a type is checked against one of its supertypes, for example. // Therefore, handle direct type checks with care or avoid it altogether and use TypeSwitch() or IsForType() instead. func (e *Error) Type() *Type { cause := e for cause != nil { if !cause.transparent { return cause.errorType } cause = Cast(cause.Cause()) } return foreignType } // Message returns a message of this particular error, disregarding the cause. // The result of this method, like a result of an Error() method, should never be used to infer the meaning of an error. // In most cases, message is only used as a part of formatting to print error contents into a log file. // Manual extraction may be required, however, to transform an error into another format - say, API response. func (e *Error) Message() string { return e.message } // Cause returns the immediate (wrapped) cause of current error. // This method could be used to dig for root cause of the error, but it is not advised to do so. // Errors should not require a complex navigation through causes to be properly handled, and the need to do so is a code smell. // Manually extracting cause defeats features such as opaque wrap, behaviour of properties etc. // This method is, therefore, reserved for system utilities, not for general use. func (e *Error) Cause() error { return e.cause } // Is returns true if and only if target is errorx error that passes errorx type check against current error. // This behaviour is exactly the same as that of IsOfType(). // See also: errors.Is() func (e *Error) Is(target error) bool { typedTarget := Cast(target) return typedTarget != nil && IsOfType(e, typedTarget.Type()) } // From errors package: if e.Unwrap() returns a non-nil error w, then we say that e wraps w. // Unwrap returns cause of current error in case it is wrapped transparently, nil otherwise. // See also: errors.Unwrap() func (e *Error) Unwrap() error { if e != nil && e.cause != nil && e.transparent { return e.cause } else { return nil } } // Format implements the Formatter interface. // Supported verbs: // // %s simple message output // %v same as %s // %+v full output complete with a stack trace // // In is nearly always preferable to use %+v format. // If a stack trace is not required, it should be omitted at the moment of creation rather in formatting. func (e *Error) Format(s fmt.State, verb rune) { message := e.fullMessage() switch verb { case 'v': _, _ = io.WriteString(s, message) if s.Flag('+') { e.stackTrace.Format(s, verb) } case 's': _, _ = io.WriteString(s, message) } } // Error implements the error interface. // A result is the same as with %s formatter and does not contain a stack trace. func (e *Error) Error() string { return e.fullMessage() } func (e *Error) fullMessage() string { if e.transparent { return e.messageWithUnderlyingInfo() } return joinStringsIfNonEmpty(": ", e.errorType.FullName(), e.messageWithUnderlyingInfo()) } func (e *Error) messageWithUnderlyingInfo() string { return joinStringsIfNonEmpty(" ", e.messageText(), e.underlyingInfo()) } func (e *Error) underlyingInfo() string { if !e.hasUnderlying { return "" } underlying := e.underlying() infos := make([]string, 0, len(underlying)) for _, err := range underlying { infos = append(infos, err.Error()) } return fmt.Sprintf("(hidden: %s)", joinStringsIfNonEmpty(", ", infos...)) } func (e *Error) messageFromProperties() string { if e.printablePropertyCount == 0 { return "" } uniq := make(map[Property]struct{}, e.printablePropertyCount) strs := make([]string, 0, e.printablePropertyCount) for m := e.properties; m != nil; m = m.next { if !m.p.printable { continue } if _, ok := uniq[m.p]; ok { continue } uniq[m.p] = struct{}{} strs = append(strs, fmt.Sprintf("%s: %v", m.p.label, m.value)) } return "{" + strings.Join(strs, ", ") + "}" } func (e *Error) underlying() []error { if !e.hasUnderlying { return nil } // Note: properties are used as storage for optional "underlying errors". // Chain of cause should not be traversed here. u, _ := e.properties.get(propertyUnderlying) return u.([]error) } func (e *Error) messageText() string { message := joinStringsIfNonEmpty(" ", e.message, e.messageFromProperties()) if cause := e.Cause(); cause != nil { return joinStringsIfNonEmpty(", cause: ", message, cause.Error()) } return message } errorx-1.2.0/error_112.go000066400000000000000000000005031471340372300151000ustar00rootroot00000000000000// +build !go1.13 package errorx func isOfType(err error, t *Type) bool { e := Cast(err) return e != nil && e.IsOfType(t) } func (e *Error) isOfType(t *Type) bool { cause := e for cause != nil { if !cause.transparent { return cause.errorType.IsOfType(t) } cause = Cast(cause.Cause()) } return false } errorx-1.2.0/error_113.go000066400000000000000000000011401471340372300150770ustar00rootroot00000000000000// +build go1.13 package errorx import "errors" func isOfType(err error, t *Type) bool { e := burrowForTyped(err) return e != nil && e.IsOfType(t) } func (e *Error) isOfType(t *Type) bool { cause := e for cause != nil { if !cause.transparent { return cause.errorType.IsOfType(t) } cause = burrowForTyped(cause.Cause()) } return false } // burrowForTyped returns either the first *Error in unwrap chain or nil func burrowForTyped(err error) *Error { raw := err for raw != nil { typed := Cast(raw) if typed != nil { return typed } raw = errors.Unwrap(raw) } return nil } errorx-1.2.0/error_113_test.go000066400000000000000000000075551471340372300161560ustar00rootroot00000000000000// +build go1.13 package errorx import ( "errors" "fmt" "io" "testing" "github.com/stretchr/testify/require" ) func TestErrorUnwrap(t *testing.T) { t.Run("Trivial", func(t *testing.T) { err := testType.NewWithNoMessage() unwrapped := errors.Unwrap(err) require.Nil(t, unwrapped) }) t.Run("Wrap", func(t *testing.T) { err := testTypeBar1.Wrap(testType.NewWithNoMessage(), "") unwrapped := errors.Unwrap(err) require.Nil(t, unwrapped) }) t.Run("WrapForeign", func(t *testing.T) { err := testTypeBar1.Wrap(io.EOF, "") unwrapped := errors.Unwrap(err) require.Nil(t, unwrapped) }) t.Run("Decorate", func(t *testing.T) { err := Decorate(testType.NewWithNoMessage(), "") unwrapped := errors.Unwrap(err) require.NotNil(t, unwrapped) require.True(t, IsOfType(unwrapped, testType)) require.True(t, Cast(unwrapped).Type() == testType) }) t.Run("DecorateForeign", func(t *testing.T) { err := Decorate(io.EOF, "") unwrapped := errors.Unwrap(err) require.NotNil(t, unwrapped) require.True(t, errors.Is(unwrapped, io.EOF)) require.True(t, unwrapped == io.EOF) }) t.Run("Nested", func(t *testing.T) { err := Decorate(Decorate(testType.NewWithNoMessage(), ""), "") unwrapped := errors.Unwrap(err) require.NotNil(t, unwrapped) unwrapped = errors.Unwrap(unwrapped) require.NotNil(t, unwrapped) require.True(t, IsOfType(unwrapped, testType)) }) t.Run("NestedWrapped", func(t *testing.T) { err := Decorate(testTypeBar1.Wrap(testType.NewWithNoMessage(), ""), "") unwrapped := errors.Unwrap(err) require.NotNil(t, unwrapped) require.True(t, IsOfType(unwrapped, testTypeBar1)) unwrapped = errors.Unwrap(unwrapped) require.Nil(t, unwrapped) }) t.Run("NestedForeign", func(t *testing.T) { err := Decorate(Decorate(io.EOF, ""), "") unwrapped := errors.Unwrap(err) require.NotNil(t, unwrapped) unwrapped = errors.Unwrap(unwrapped) require.NotNil(t, unwrapped) require.True(t, errors.Is(unwrapped, io.EOF)) }) } func TestErrorIs(t *testing.T) { t.Run("Trivial", func(t *testing.T) { err := testType.NewWithNoMessage() require.True(t, errors.Is(err, testType.NewWithNoMessage())) require.False(t, errors.Is(err, testTypeBar1.NewWithNoMessage())) }) t.Run("Wrap", func(t *testing.T) { err := testTypeBar1.Wrap(testType.NewWithNoMessage(), "") require.False(t, errors.Is(err, testType.NewWithNoMessage())) require.True(t, errors.Is(err, testTypeBar1.NewWithNoMessage())) }) t.Run("Supertype", func(t *testing.T) { err := testSubtype0.Wrap(testTypeBar1.NewWithNoMessage(), "") require.True(t, errors.Is(err, testType.NewWithNoMessage())) require.True(t, errors.Is(err, testSubtype0.NewWithNoMessage())) require.False(t, errors.Is(err, testTypeBar1.NewWithNoMessage())) }) t.Run("Decorate", func(t *testing.T) { err := Decorate(testType.NewWithNoMessage(), "") require.True(t, errors.Is(err, testType.NewWithNoMessage())) }) t.Run("DecorateForeign", func(t *testing.T) { err := Decorate(io.EOF, "") require.True(t, errors.Is(err, io.EOF)) }) } func TestErrorsAndErrorx(t *testing.T) { t.Run("DecoratedForeign", func(t *testing.T) { err := fmt.Errorf("error test: %w", testType.NewWithNoMessage()) require.True(t, errors.Is(err, testType.NewWithNoMessage())) require.True(t, IsOfType(err, testType)) }) t.Run("LayeredDecorate", func(t *testing.T) { err := Decorate(fmt.Errorf("error test: %w", testType.NewWithNoMessage()), "test") require.True(t, errors.Is(err, testType.NewWithNoMessage())) require.True(t, IsOfType(err, testType)) }) t.Run("LayeredDecorateAgain", func(t *testing.T) { err := fmt.Errorf("error test: %w", Decorate(io.EOF, "test")) require.True(t, errors.Is(err, io.EOF)) }) t.Run("Wrap", func(t *testing.T) { err := fmt.Errorf("error test: %w", testType.Wrap(io.EOF, "test")) require.False(t, errors.Is(err, io.EOF)) require.True(t, errors.Is(err, testType.NewWithNoMessage())) }) }errorx-1.2.0/error_test.go000066400000000000000000000130021471340372300155520ustar00rootroot00000000000000package errorx import ( "errors" "fmt" "testing" "github.com/stretchr/testify/require" ) var ( testNamespace = NewNamespace("foo") testType = testNamespace.NewType("bar") testTypeSilent = testType.NewSubtype("silent").ApplyModifiers(TypeModifierOmitStackTrace) testTypeTransparent = testType.NewSubtype("transparent").ApplyModifiers(TypeModifierTransparent) testTypeSilentTransparent = testType.NewSubtype("silent_transparent").ApplyModifiers(TypeModifierTransparent, TypeModifierOmitStackTrace) testSubtype0 = testType.NewSubtype("internal") testSubtype1 = testSubtype0.NewSubtype("wat") testTypeBar1 = testNamespace.NewType("bar1") testTypeBar2 = testNamespace.NewType("bar2") ) func TestError(t *testing.T) { err := testType.NewWithNoMessage() require.Equal(t, "foo.bar", err.Error()) } func TestErrorWithMessage(t *testing.T) { err := testType.New("oops") require.Equal(t, "foo.bar: oops", err.Error()) } func TestErrorMessageWithCause(t *testing.T) { err := testSubtype1.WrapWithNoMessage(testType.New("fatal")) require.Equal(t, "foo.bar.internal.wat: foo.bar: fatal", err.Error()) } func TestErrorWrap(t *testing.T) { err0 := testType.NewWithNoMessage() err1 := testTypeBar1.Wrap(err0, "a") require.Nil(t, Ignore(err1, testTypeBar1)) require.NotNil(t, Ignore(err1, testType)) } func TestErrorDecorate(t *testing.T) { err0 := testType.NewWithNoMessage() err1 := testTypeBar1.Wrap(err0, "a") err2 := Decorate(err1, "b") require.NotNil(t, Ignore(err2, testTypeBar2)) require.Nil(t, Ignore(err2, testTypeBar1)) require.NotNil(t, Ignore(err2, testType)) } func TestErrorMessages(t *testing.T) { t.Run("Subtypes", func(t *testing.T) { require.Equal(t, "foo.bar.internal.wat", testSubtype1.NewWithNoMessage().Error()) require.Equal(t, "foo.bar.internal.wat: oops", testSubtype1.New("oops").Error()) }) t.Run("Wrapped", func(t *testing.T) { cause := testType.New("poof!") require.Equal(t, "foo.bar.internal.wat: foo.bar: poof!", testSubtype1.Wrap(cause, "").Error()) require.Equal(t, "foo.bar.internal.wat: foo.bar: poof!", testSubtype1.WrapWithNoMessage(cause).Error()) require.Equal(t, "foo.bar.internal.wat: oops, cause: foo.bar: poof!", testSubtype1.Wrap(cause, "oops").Error()) }) t.Run("Complex", func(t *testing.T) { innerCause := NewNamespace("c").NewType("d").Wrap(errors.New("Achtung!"), "panic") stackedError := testSubtype1.Wrap(testType.Wrap(innerCause, "poof!"), "") require.Equal(t, "foo.bar.internal.wat: foo.bar: poof!, cause: c.d: panic, cause: Achtung!", stackedError.Error()) }) } func TestImmutableError(t *testing.T) { t.Run("Property", func(t *testing.T) { err := testType.NewWithNoMessage() err1 := err.WithProperty(PropertyPayload(), 1) err2 := err1.WithProperty(PropertyPayload(), 2) require.True(t, err.errorType.IsOfType(err2.errorType)) require.Equal(t, err.message, err2.message) payload, ok := ExtractPayload(err) require.False(t, ok) payload, ok = ExtractPayload(err1) require.True(t, ok) require.EqualValues(t, 1, payload) payload, ok = ExtractPayload(err2) require.True(t, ok) require.EqualValues(t, 2, payload) }) t.Run("Underlying", func(t *testing.T) { err := testType.NewWithNoMessage() err1 := err.WithUnderlyingErrors(testSubtype0.NewWithNoMessage()) err2 := err1.WithUnderlyingErrors(testSubtype1.NewWithNoMessage()) require.True(t, err.errorType.IsOfType(err2.errorType)) require.Equal(t, err.message, err2.message) require.Len(t, err.underlying(), 0) require.Len(t, err1.underlying(), 1) require.Len(t, err2.underlying(), 2) }) } func TestErrorStackTrace(t *testing.T) { err := createErrorFuncInStackTrace(testType) output := fmt.Sprintf("%+v", err) require.Contains(t, output, "createErrorFuncInStackTrace", output) require.Contains(t, output, "TestErrorStackTrace", output) } func TestEnhancedStackTrace(t *testing.T) { err := createWrappedErrorFuncOuterInStackTrace(testType) output := fmt.Sprintf("%+v", err) require.Contains(t, output, "createWrappedErrorFuncOuterInStackTrace", output) require.Contains(t, output, "createErrorInAnotherGoroutine", output) } func TestDecorate(t *testing.T) { err := Decorate(testType.NewWithNoMessage(), "ouch!") require.Equal(t, "ouch!, cause: foo.bar", err.Error()) require.True(t, IsOfType(err, testType)) require.Equal(t, testType, err.Type()) } func TestUnderlyingInFormat(t *testing.T) { err := DecorateMany("this is terribly bad", testTypeBar1.Wrap(testSubtype1.NewWithNoMessage(), "real bad"), testTypeBar2.New("bad")) 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()) err = DecorateMany("this is terribly bad", testTypeBar1.New("real bad"), testTypeBar2.Wrap(testSubtype1.NewWithNoMessage(), "bad")) 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()) } func createErrorFuncInStackTrace(et *Type) *Error { err := et.NewWithNoMessage() return err } func createWrappedErrorFuncOuterInStackTrace(et *Type) *Error { return createWrappedErrorFuncInnerInStackTrace(et) } func createWrappedErrorFuncInnerInStackTrace(et *Type) *Error { channel := make(chan *Error) go func() { createErrorInAnotherGoroutine(et, channel) }() errFromChan := <-channel return EnhanceStackTrace(errFromChan, "wrap") } func createErrorInAnotherGoroutine(et *Type, channel chan *Error) { channel <- et.NewWithNoMessage() } errorx-1.2.0/example_test.go000066400000000000000000000146401471340372300160650ustar00rootroot00000000000000package errorx_test import ( "fmt" "github.com/joomcode/errorx" ) func ExampleDecorate() { err := someFunc() fmt.Println(err.Error()) err = errorx.Decorate(err, "decorate") fmt.Println(err.Error()) err = errorx.Decorate(err, "outer decorate") fmt.Println(err.Error()) // Output: common.assertion_failed: example // decorate, cause: common.assertion_failed: example // outer decorate, cause: decorate, cause: common.assertion_failed: example } func ExampleDecorateMany() { err0 := someFunc() err1 := someFunc() err := errorx.DecorateMany("both calls failed", err0, err1) fmt.Println(err.Error()) // Output: both calls failed, cause: common.assertion_failed: example (hidden: common.assertion_failed: example) } func ExampleError_WithUnderlyingErrors() { fn := func() error { bytes, err := getBodyAndError() if err != nil { _, unmarshalErr := getDetailsFromBody(bytes) if unmarshalErr != nil { return errorx.AssertionFailed.Wrap(err, "failed to read details").WithUnderlyingErrors(unmarshalErr) } } return nil } fmt.Println(fn().Error()) // Output: common.assertion_failed: failed to read details, cause: common.assertion_failed: example (hidden: common.illegal_format) } func ExampleType_Wrap() { originalErr := errorx.IllegalArgument.NewWithNoMessage() err := errorx.AssertionFailed.Wrap(originalErr, "wrapped") fmt.Println(errorx.IsOfType(originalErr, errorx.IllegalArgument)) fmt.Println(errorx.IsOfType(err, errorx.IllegalArgument)) fmt.Println(errorx.IsOfType(err, errorx.AssertionFailed)) fmt.Println(err.Error()) // Output: // true // false // true // common.assertion_failed: wrapped, cause: common.illegal_argument } func ExampleError_Format() { err := nestedCall() simpleOutput := fmt.Sprintf("Error short: %v\n", err) verboseOutput := fmt.Sprintf("Error full: %+v", err) fmt.Println(simpleOutput) fmt.Println(verboseOutput) // Example output: //Error short: common.assertion_failed: example // //Error full: common.assertion_failed: example // at github.com/joomcode/errorx_test.someFunc() // /Users/username/go/src/github.com/joomcode/errorx/example_test.go:102 // at github.com/joomcode/errorx_test.nestedCall() // /Users/username/go/src/github.com/joomcode/errorx/example_test.go:98 // at github.com/joomcode/errorx_test.ExampleError_Format() // /Users/username/go/src/github.com/joomcode/errorx/example_test.go:66 // <...> more } func ExampleEnhanceStackTrace() { errCh := make(chan error) go func() { errCh <- nestedCall() }() err := <-errCh verboseOutput := fmt.Sprintf("Error full: %+v", errorx.EnhanceStackTrace(err, "another goroutine")) fmt.Println(verboseOutput) // Example output: //Error full: another goroutine, cause: common.assertion_failed: example // at github.com/joomcode/errorx_test.ExampleEnhanceStackTrace() // /Users/username/go/src/github.com/joomcode/errorx/example_test.go:94 // at testing.runExample() // /usr/local/Cellar/go/1.10.3/libexec/src/testing/example.go:122 // at testing.runExamples() // /usr/local/Cellar/go/1.10.3/libexec/src/testing/example.go:46 // at testing.(*M).Run() // /usr/local/Cellar/go/1.10.3/libexec/src/testing/testing.go:979 // at main.main() // _testmain.go:146 // ... // (1 duplicated frames) // ---------------------------------- // at github.com/joomcode/errorx_test.someFunc() // /Users/username/go/src/github.com/joomcode/errorx/example_test.go:106 // at github.com/joomcode/errorx_test.nestedCall() // /Users/username/go/src/github.com/joomcode/errorx/example_test.go:102 // at github.com/joomcode/errorx_test.ExampleEnhanceStackTrace.func1() // /Users/username/go/src/github.com/joomcode/errorx/example_test.go:90 // at runtime.goexit() // /usr/local/Cellar/go/1.10.3/libexec/src/runtime/asm_amd64.s:2361 } func ExampleIgnore() { err := errorx.IllegalArgument.NewWithNoMessage() err = errorx.Decorate(err, "more info") fmt.Println(err) fmt.Println(errorx.Ignore(err, errorx.IllegalArgument)) fmt.Println(errorx.Ignore(err, errorx.AssertionFailed)) // Output: // more info, cause: common.illegal_argument // // more info, cause: common.illegal_argument } func ExampleIgnoreWithTrait() { err := errorx.TimeoutElapsed.NewWithNoMessage() err = errorx.Decorate(err, "more info") fmt.Println(err) fmt.Println(errorx.IgnoreWithTrait(err, errorx.Timeout())) fmt.Println(errorx.IgnoreWithTrait(err, errorx.NotFound())) // Output: // more info, cause: common.timeout // // more info, cause: common.timeout } func ExampleIsOfType() { err0 := errorx.DataUnavailable.NewWithNoMessage() err1 := errorx.Decorate(err0, "decorated") err2 := errorx.RejectedOperation.Wrap(err0, "wrapped") fmt.Println(errorx.IsOfType(err0, errorx.DataUnavailable)) fmt.Println(errorx.IsOfType(err1, errorx.DataUnavailable)) fmt.Println(errorx.IsOfType(err2, errorx.DataUnavailable)) // Output: // true // true // false } func ExampleTypeSwitch() { err := errorx.DataUnavailable.NewWithNoMessage() switch errorx.TypeSwitch(err, errorx.DataUnavailable) { case errorx.DataUnavailable: fmt.Println("good") case nil: fmt.Println("bad") default: fmt.Println("bad") } switch errorx.TypeSwitch(nil, errorx.DataUnavailable) { case errorx.DataUnavailable: fmt.Println("bad") case nil: fmt.Println("good") default: fmt.Println("bad") } switch errorx.TypeSwitch(err, errorx.TimeoutElapsed) { case errorx.TimeoutElapsed: fmt.Println("bad") case nil: fmt.Println("bad") default: fmt.Println("good") } // Output: // good // good // good } func ExampleTraitSwitch() { err := errorx.TimeoutElapsed.NewWithNoMessage() switch errorx.TraitSwitch(err, errorx.Timeout()) { case errorx.Timeout(): fmt.Println("good") case errorx.CaseNoError(): fmt.Println("bad") default: fmt.Println("bad") } switch errorx.TraitSwitch(nil, errorx.Timeout()) { case errorx.Timeout(): fmt.Println("bad") case errorx.CaseNoError(): fmt.Println("good") default: fmt.Println("bad") } switch errorx.TraitSwitch(err, errorx.NotFound()) { case errorx.NotFound(): fmt.Println("bad") case errorx.CaseNoError(): fmt.Println("bad") default: fmt.Println("good") } // Output: // good // good // good } func nestedCall() error { return someFunc() } func someFunc() error { return errorx.AssertionFailed.New("example") } func getBodyAndError() ([]byte, error) { return nil, errorx.AssertionFailed.New("example") } func getDetailsFromBody(s []byte) (string, error) { return "", errorx.IllegalFormat.New(string(s)) } errorx-1.2.0/go.mod000066400000000000000000000002131471340372300141410ustar00rootroot00000000000000module github.com/joomcode/errorx go 1.11 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/stretchr/testify v1.4.0 ) errorx-1.2.0/go.sum000066400000000000000000000020411471340372300141670ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= errorx-1.2.0/helper.go000066400000000000000000000010611471340372300146430ustar00rootroot00000000000000package errorx import "strings" func joinStringsIfNonEmpty(delimiter string, parts ...string) string { switch len(parts) { case 0: return "" case 1: return parts[0] case 2: if len(parts[0]) == 0 { return parts[1] } else if len(parts[1]) == 0 { return parts[0] } else { return parts[0] + delimiter + parts[1] } default: filteredParts := make([]string, 0, len(parts)) for _, part := range parts { if len(part) > 0 { filteredParts = append(filteredParts, part) } } return strings.Join(filteredParts, delimiter) } } errorx-1.2.0/id.go000066400000000000000000000004551471340372300137660ustar00rootroot00000000000000package errorx import ( "sync/atomic" ) var internalID uint64 // nextInternalID creates next unique id for errorx entities. // All equality comparison should take id into account, lest there be some false positive matches. func nextInternalID() uint64 { return atomic.AddUint64(&internalID, 1) } errorx-1.2.0/modifier.go000066400000000000000000000047221471340372300151710ustar00rootroot00000000000000package errorx // TypeModifier is a way to change a default behaviour for an error type, directly or via type hierarchy. // Modification is intentionally one-way, as it provides much more clarity. // If there is a modifier on a type or a namespace, all its descendants definitely have the same default behaviour. // If some of a subtypes must lack a specific modifier, then the modifier must be removed from the common ancestor. type TypeModifier int const ( // TypeModifierTransparent is a type modifier; an error type with such modifier creates transparent wrappers by default TypeModifierTransparent TypeModifier = 1 // TypeModifierOmitStackTrace is a type modifier; an error type with such modifier omits the stack trace collection upon creation of an error instance TypeModifierOmitStackTrace TypeModifier = 2 ) type modifiers interface { CollectStackTrace() bool Transparent() bool ReplaceWith(new modifiers) modifiers } var _ modifiers = noModifiers{} var _ modifiers = typeModifiers{} var _ modifiers = inheritedModifiers{} type noModifiers struct { } func (noModifiers) CollectStackTrace() bool { return true } func (noModifiers) Transparent() bool { return false } func (noModifiers) ReplaceWith(new modifiers) modifiers { return new } type typeModifiers struct { omitStackTrace bool transparent bool } func newTypeModifiers(modifiers ...TypeModifier) modifiers { m := typeModifiers{} for _, modifier := range modifiers { switch modifier { case TypeModifierOmitStackTrace: m.omitStackTrace = true case TypeModifierTransparent: m.transparent = true } } return m } func (m typeModifiers) CollectStackTrace() bool { return !m.omitStackTrace } func (m typeModifiers) Transparent() bool { return m.transparent } func (typeModifiers) ReplaceWith(new modifiers) modifiers { panic("attempt to modify type modifiers the second time") } type inheritedModifiers struct { parent modifiers override modifiers } func newInheritedModifiers(modifiers modifiers) modifiers { if _, ok := modifiers.(noModifiers); ok { return noModifiers{} } return inheritedModifiers{ parent: modifiers, override: noModifiers{}, } } func (m inheritedModifiers) CollectStackTrace() bool { return m.parent.CollectStackTrace() && m.override.CollectStackTrace() } func (m inheritedModifiers) Transparent() bool { return m.parent.Transparent() || m.override.Transparent() } func (m inheritedModifiers) ReplaceWith(new modifiers) modifiers { m.override = new return m } errorx-1.2.0/modifier_test.go000066400000000000000000000037421471340372300162310ustar00rootroot00000000000000package errorx import ( "fmt" "testing" "github.com/stretchr/testify/require" ) var ( modifierTestNamespace = NewNamespace("modifier") modifierTestNamespaceTransparent = NewNamespace("modifierTransparent").ApplyModifiers(TypeModifierTransparent) modifierTestNamespaceTransparentChild = modifierTestNamespaceTransparent.NewSubNamespace("child") modifierTestError = modifierTestNamespace.NewType("foo") modifierTestErrorNoTrace = modifierTestNamespace.NewType("bar").ApplyModifiers(TypeModifierOmitStackTrace) modifierTestErrorNoTraceChild = modifierTestErrorNoTrace.NewSubtype("child") modifierTestErrorTransparent = modifierTestNamespaceTransparent.NewType("simple") modifierTestErrorGrandchild = modifierTestNamespaceTransparentChild.NewType("all").ApplyModifiers(TypeModifierOmitStackTrace) ) func TestTypeModifier(t *testing.T) { t.Run("Default", func(t *testing.T) { err := modifierTestError.New("test") output := fmt.Sprintf("%+v", err) require.Contains(t, output, "errorx/modifier_test.go") }) t.Run("NoTrace", func(t *testing.T) { err := modifierTestErrorNoTrace.New("test") output := fmt.Sprintf("%+v", err) require.NotContains(t, output, "errorx/modifier_test.go") }) } func TestTypeModifierInheritance(t *testing.T) { t.Run("Type", func(t *testing.T) { err := modifierTestErrorNoTraceChild.New("test") output := fmt.Sprintf("%+v", err) require.NotContains(t, output, "errorx/modifier_test.go") }) t.Run("Namespace", func(t *testing.T) { err := modifierTestErrorTransparent.Wrap(AssertionFailed.New("test"), "boo") require.True(t, err.IsOfType(AssertionFailed)) }) t.Run("Deep", func(t *testing.T) { err := modifierTestErrorGrandchild.Wrap(AssertionFailed.New("test"), "boo") require.True(t, err.IsOfType(AssertionFailed)) err = modifierTestErrorGrandchild.New("test") output := fmt.Sprintf("%+v", err) require.NotContains(t, output, "errorx/modifier_test.go") }) } errorx-1.2.0/namespace.go000066400000000000000000000064761471340372300153370ustar00rootroot00000000000000package errorx import "fmt" // Namespace is a way go group a number of error types together, and each error type belongs to exactly one namespace. // Namespaces may form hierarchy, with child namespaces inheriting the traits and modifiers of a parent. // Those modifiers and traits are then passed upon all error types in the namespace. // In formatting, a dot notation is used, for example: // // namespace.sub_namespace.type.subtype // type Namespace struct { parent *Namespace id uint64 name string traits []Trait modifiers modifiers } // NamespaceKey is a comparable descriptor of a Namespace. type NamespaceKey struct { id uint64 } // NewNamespace defines a namespace with a name and, optionally, a number of inheritable traits. func NewNamespace(name string, traits ...Trait) Namespace { namespace := newNamespace(nil, name, traits...) globalRegistry.registerNamespace(namespace) return namespace } // NewSubNamespace defines a child namespace that inherits all that is defined for a parent and, optionally, adds some more. func (n Namespace) NewSubNamespace(name string, traits ...Trait) Namespace { namespace := newNamespace(&n, name, traits...) globalRegistry.registerNamespace(namespace) return namespace } // ApplyModifiers makes a one-time modification of defaults in error creation. func (n Namespace) ApplyModifiers(modifiers ...TypeModifier) Namespace { n.modifiers = n.modifiers.ReplaceWith(newTypeModifiers(modifiers...)) return n } // NewType creates a new type within a namespace that inherits all that is defined for namespace and, optionally, adds some more. func (n Namespace) NewType(typeName string, traits ...Trait) *Type { return NewType(n, typeName, traits...) } // Key returns a comparison key for namespace. func (n Namespace) Key() NamespaceKey { return NamespaceKey{ id: n.id, } } // IsNamespaceOf checks whether or not an error belongs either to this namespace or some of its sub-namespaces. func (n Namespace) IsNamespaceOf(t *Type) bool { namespace := t.namespace other := &namespace for other != nil { if n.Key() == other.Key() { return true } other = other.parent } return false } // FullName returns a full name of a namespace. func (n Namespace) FullName() string { return n.name } func (n Namespace) String() string { return n.name } // Parent returns the immediate parent namespace, if present. // The use of this function outside of a system layer that handles error types (see TypeSubscriber) is a code smell. func (n Namespace) Parent() *Namespace { return n.parent } func (n Namespace) collectTraits() map[Trait]bool { result := make(map[Trait]bool) namespace := &n for namespace != nil { for _, trait := range namespace.traits { result[trait] = true } namespace = namespace.parent } return result } func newNamespace(parent *Namespace, name string, traits ...Trait) Namespace { createName := func() string { if parent == nil { return name } return fmt.Sprintf("%s.%s", parent.FullName(), name) } createModifiers := func() modifiers { if parent == nil { return noModifiers{} } return newInheritedModifiers(parent.modifiers) } namespace := Namespace{ id: nextInternalID(), parent: parent, name: createName(), traits: append([]Trait(nil), traits...), modifiers: createModifiers(), } return namespace } errorx-1.2.0/namespace_test.go000066400000000000000000000032121471340372300163570ustar00rootroot00000000000000package errorx import ( "testing" "github.com/stretchr/testify/require" ) var ( nsTest0 = NewNamespace("nsTest0") nsTest1 = NewNamespace("nsTest1") nsTest1Child = nsTest1.NewSubNamespace("child") nsTestET0 = nsTest0.NewType("type0") nsTestET1 = nsTest1.NewType("type1") nsTestET1Child = nsTestET1.NewSubtype("child") nsTestChild1ET = nsTest1Child.NewType("type") nsTestChild1ETChild = nsTestChild1ET.NewSubtype("child") ) func TestNamespaceName(t *testing.T) { require.EqualValues(t, "nsTest1", nsTest1.FullName()) require.EqualValues(t, "nsTest1.child", nsTest1Child.FullName()) } func TestIsNamespaceOf(t *testing.T) { require.True(t, nsTest0.IsNamespaceOf(nsTestET0)) require.False(t, nsTest1.IsNamespaceOf(nsTestET0)) require.False(t, nsTest0.IsNamespaceOf(nsTestET1)) require.True(t, nsTest1.IsNamespaceOf(nsTestET1)) } func TestNamespaceSubtype(t *testing.T) { require.False(t, nsTest0.IsNamespaceOf(nsTestET1Child)) require.True(t, nsTest1.IsNamespaceOf(nsTestET1Child)) } func TestSubNamespace(t *testing.T) { require.False(t, nsTest1Child.IsNamespaceOf(nsTestET1)) require.True(t, nsTest1Child.IsNamespaceOf(nsTestChild1ET)) require.True(t, nsTest1Child.IsNamespaceOf(nsTestChild1ETChild)) } func TestRootNamespace(t *testing.T) { require.Equal(t, nsTest1, nsTestChild1ET.NewWithNoMessage().Type().RootNamespace()) } func TestNamespace(t *testing.T) { require.Equal(t, nsTest1Child, nsTestChild1ET.NewWithNoMessage().Type().Namespace()) } func TestSubTypeNamespaceFullName(t *testing.T) { require.Equal(t, "nsTest1.child", nsTestChild1ETChild.Namespace().FullName()) } errorx-1.2.0/panic.go000066400000000000000000000050701471340372300144620ustar00rootroot00000000000000package errorx import "fmt" // Panic is an alternative to the built-in panic call. // When calling panic as a reaction to error, prefer this function over vanilla panic(). // If err happens to be an errorx error, it may hold the original stack trace of the issue. // With panic(err), this information may be lost if panic is handled by the default handler. // With errorx.Panic(err), all data is preserved regardless of the handle mechanism. // It can be recovered either from default panic message, recover() result or ErrorFromPanic() function. // // Even if err stack trace is exactly the same as default panic trace, this can be tolerated, // as panics must not be a way to report conventional errors and are therefore rare. // With this in mind, it is better to err on the side of completeness rather than brevity. // // This function never returns, but the signature may be used for convenience: // // return nil, errorx.Panic(err) // panic(errorx.Panic(err)) // func Panic(err error) error { panic(newPanicErrorWrapper(err)) } // ErrorFromPanic recovers the original error from panic, best employed along with Panic() function from the same package. // The original error, if present, typically holds more relevant data // than a combination of panic message and the stack trace which can be collected after recover(). // // More importantly, it allows for greater composability, // if ever there is a need to recover from panic and pass the error information forwards in its proper form. // // Note that panic is not a proper means to report errors, // so this mechanism should never be used where a error based control flow is at all possible. func ErrorFromPanic(recoverResult interface{}) (error, bool) { err, ok := recoverResult.(error) if !ok { return nil, false } if wrapper, ok := err.(*panicErrorWrapper); ok { return wrapper.inner, true } return err, true } func newPanicErrorWrapper(err error) *panicErrorWrapper { return &panicErrorWrapper{ inner: NewErrorBuilder(panicPayloadWrap). WithConditionallyFormattedMessage("panic"). WithCause(err). EnhanceStackTrace(). Create(), } } // panicErrorWrapper is designed for the original stack trace not to be lost in any way it may be handled type panicErrorWrapper struct { inner error } func (w *panicErrorWrapper) Error() string { return fmt.Sprintf("%+v", w.inner) } func (w *panicErrorWrapper) String() string { return w.Error() } // Only required to transform panic into error while preserving the stack trace var panicPayloadWrap = syntheticErrors.NewType("panic").ApplyModifiers(TypeModifierTransparent) errorx-1.2.0/panic_test.go000066400000000000000000000070421471340372300155220ustar00rootroot00000000000000package errorx import ( "errors" "fmt" "testing" "time" "github.com/stretchr/testify/require" ) func TestPanic(t *testing.T) { defer func() { r := recover() require.NotNil(t, r) output := fmt.Sprintf("%v", r) require.Contains(t, output, "errorx.funcWithErr()", output) }() Panic(funcWithErr()) } func TestPanicErrorx(t *testing.T) { defer func() { r := recover() require.NotNil(t, r) output := fmt.Sprintf("%v", r) require.Contains(t, output, "awful", output) require.Contains(t, output, "errorx.funcWithBadPanic()", output) }() funcWithBadPanic() } func TestPanicRecover(t *testing.T) { defer func() { r := recover() require.NotNil(t, r) err, ok := ErrorFromPanic(r) require.True(t, ok) output := fmt.Sprintf("%+v", err) require.Contains(t, output, "errorx.funcWithErr()", output) require.Contains(t, output, "bad", output) require.True(t, IsOfType(err, testType)) }() Panic(funcWithErr()) } func TestPanicRecoverNoTrace(t *testing.T) { defer func() { r := recover() require.NotNil(t, r) err, ok := ErrorFromPanic(r) require.True(t, ok) output := fmt.Sprintf("%+v", err) require.NotContains(t, output, "errorx.funcWithErrNoTrace()", output) require.Contains(t, output, "errorx.funcWithPanicNoTrace()", output) require.Contains(t, output, "silent", output) require.True(t, IsOfType(err, testType)) }() funcWithPanicNoTrace() } func TestPanicRecoverNoErrorx(t *testing.T) { defer func() { r := recover() require.NotNil(t, r) err, ok := ErrorFromPanic(r) require.True(t, ok) output := fmt.Sprintf("%+v", err) require.NotContains(t, output, "errorx.funcWithBadErr()", output) require.Contains(t, output, "errorx.funcWithBadPanic()", output) require.Contains(t, output, "awful", output) require.False(t, IsOfType(err, testType)) }() funcWithBadPanic() } func funcWithErr() error { return testType.New("bad") } func funcWithPanicNoTrace() { Panic(funcWithErrNoTrace()) } func funcWithErrNoTrace() error { return testTypeSilent.New("silent") } func funcWithBadPanic() { Panic(funcWithBadErr()) } func funcWithBadErr() error { return errors.New("awful") } func TestPanicChain(t *testing.T) { ch0 := make(chan error, 1) ch1 := make(chan error, 1) go doMischief(ch1) go doMoreMischief(ch0, ch1) select { case err := <-ch0: require.Error(t, err) require.False(t, IsOfType(err, AssertionFailed)) output := fmt.Sprintf("%+v", err) require.Contains(t, output, "mischiefProper", output) require.Contains(t, output, "mischiefAsPanic", output) require.Contains(t, output, "doMischief", output) require.Contains(t, output, "handleMischief", output) require.NotContains(t, output, "doMoreMischief", output) // stack trace is only enhanced in Panic, not in user code t.Log(output) case <-time.After(time.Second): require.Fail(t, "expected error") } } func doMoreMischief(ch0 chan error, ch1 chan error) { defer func() { if e := recover(); e != nil { err, ok := ErrorFromPanic(e) if ok { ch0 <- Decorate(err, "hop 2") return } } ch0 <- AssertionFailed.New("test failed") }() handleMischief(ch1) } func handleMischief(ch chan error) { err := <-ch Panic(Decorate(err, "handle")) } func doMischief(ch chan error) { defer func() { if e := recover(); e != nil { err, ok := ErrorFromPanic(e) if ok { ch <- Decorate(err, "hop 1") return } } ch <- AssertionFailed.New("test failed") }() mischiefAsPanic() } func mischiefAsPanic() { Panic(mischiefProper()) } func mischiefProper() error { return ExternalError.New("mischief") } errorx-1.2.0/property.go000066400000000000000000000064531471340372300152620ustar00rootroot00000000000000package errorx import ( "context" ) // Property is a key to a dynamic property of an error. // Property value belongs to an error instance only, never inherited from a type. // Property visibility is hindered by Wrap, preserved by Decorate. type Property struct { *property // Property is compared by this pointer. } type property struct { label string printable bool } // RegisterProperty registers a new property key. // It is used both to add a dynamic property to an error instance, and to extract property value back from error. func RegisterProperty(label string) Property { return newProperty(label, false) } // RegisterPrintableProperty registers a new property key for informational value. // It is used both to add a dynamic property to an error instance, and to extract property value back from error. // Printable property will be included in Error() message, both name and value. func RegisterPrintableProperty(label string) Property { return newProperty(label, true) } // PropertyContext is a context property, value is expected to be of context.Context type. func PropertyContext() Property { return propertyContext } // PropertyPayload is a payload property, value may contain user defined structure with arbitrary data passed along with an error. func PropertyPayload() Property { return propertyPayload } // WithContext is a statically typed helper to add a context property to an error. func WithContext(err *Error, ctx context.Context) *Error { return err.WithProperty(PropertyContext(), ctx) } // ExtractContext is a statically typed helper to extract a context property from an error. func ExtractContext(err error) (context.Context, bool) { rawCtx, ok := ExtractProperty(err, PropertyContext()) if !ok { return nil, false } return rawCtx.(context.Context), true } // WithPayload is a helper to add a payload property to an error. func WithPayload(err *Error, payload interface{}) *Error { return err.WithProperty(PropertyPayload(), payload) } // ExtractPayload is a helper to extract a payload property from an error. func ExtractPayload(err error) (interface{}, bool) { return ExtractProperty(err, PropertyPayload()) } // ExtractProperty attempts to extract a property value by a provided key. // A property may belong to this error or be extracted from the original cause. func ExtractProperty(err error, key Property) (interface{}, bool) { typedErr := Cast(err) if typedErr == nil { return nil, false } return typedErr.Property(key) } var ( propertyContext = RegisterProperty("ctx") propertyPayload = RegisterProperty("payload") propertyUnderlying = RegisterProperty("underlying") ) func newProperty(label string, printable bool) Property { p := Property{ &property{ label: label, printable: printable, }, } return p } // propertyMap represents map of properties. // Compared to builtin type, it uses less allocations and reallocations on copy. // It is implemented as a simple linked list. type propertyMap struct { p Property value interface{} next *propertyMap } func (pm *propertyMap) with(p Property, value interface{}) *propertyMap { return &propertyMap{p: p, value: value, next: pm} } func (pm *propertyMap) get(p Property) (value interface{}, ok bool) { for pm != nil { if pm.p == p { return pm.value, true } pm = pm.next } return nil, false } errorx-1.2.0/property_test.go000066400000000000000000000130401471340372300163070ustar00rootroot00000000000000package errorx import ( "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNoProperty(t *testing.T) { t.Run("Simple", func(t *testing.T) { err := testType.New("test") property, ok := err.Property(PropertyPayload()) require.False(t, ok) require.Nil(t, property) }) t.Run("Decorated", func(t *testing.T) { err := testType.New("test") err = Decorate(err, "oops") property, ok := err.Property(PropertyPayload()) require.False(t, ok) require.Nil(t, property) }) t.Run("Helper", func(t *testing.T) { err := testType.New("test") property, ok := ExtractPayload(err) require.False(t, ok) require.Nil(t, property) }) } var testProperty0 = RegisterProperty("test0") var testProperty1 = RegisterProperty("test1") var testInfoProperty2 = RegisterPrintableProperty("prop2") var testInfoProperty3 = RegisterPrintableProperty("prop3") func TestProperty(t *testing.T) { t.Run("Different", func(t *testing.T) { err := testType.New("test").WithProperty(testProperty0, 42) property0, ok := err.Property(testProperty0) require.True(t, ok) require.EqualValues(t, 42, property0) property1, ok := err.Property(testProperty1) require.False(t, ok) require.Nil(t, property1) }) t.Run("Wrapped", func(t *testing.T) { err := testType.New("test").WithProperty(testProperty0, 42) err = Decorate(err, "oops") err = testTypeBar1.Wrap(err, "wrapped") property0, ok := err.Property(testProperty0) require.False(t, ok) require.Nil(t, property0) property1, ok := err.Property(testProperty1) require.False(t, ok) require.Nil(t, property1) }) t.Run("Decorated", func(t *testing.T) { err := testType.New("test").WithProperty(testProperty0, 42) err = Decorate(err, "oops") err = Decorate(err, "bad") property0, ok := err.Property(testProperty0) require.True(t, ok) require.EqualValues(t, 42, property0) property1, ok := err.Property(testProperty1) require.False(t, ok) require.Nil(t, property1) }) t.Run("FromCause", func(t *testing.T) { err := testType.New("test").WithProperty(testProperty0, 42) err = Decorate(err, "oops") err = Decorate(err, "bad").WithProperty(testProperty1, "-1") property0, ok := err.Property(testProperty0) require.True(t, ok) require.EqualValues(t, 42, property0) property1, ok := err.Property(testProperty1) require.True(t, ok) require.EqualValues(t, "-1", property1) }) t.Run("OverrideCause", func(t *testing.T) { err := testType.New("test").WithProperty(testProperty0, 42) err = Decorate(err, "oops") property0, ok := err.Property(testProperty0) require.True(t, ok) require.EqualValues(t, 42, property0) err = Decorate(err, "bad").WithProperty(testProperty0, "-1") property0, ok = err.Property(testProperty0) require.True(t, ok) require.EqualValues(t, "-1", property0) property1, ok := err.Property(testProperty1) require.False(t, ok) require.Nil(t, property1) }) } func TestPrintableProperty(t *testing.T) { err := testTypeSilent.New("test").WithProperty(testInfoProperty2, "hello world") t.Run("Simple", func(t *testing.T) { assert.Equal(t, "foo.bar.silent: test {prop2: hello world}", err.Error()) }) t.Run("Overwrite", func(t *testing.T) { err := err.WithProperty(testInfoProperty2, "cruel world") assert.Equal(t, "foo.bar.silent: test {prop2: cruel world}", err.Error()) }) t.Run("AddMore", func(t *testing.T) { err := err.WithProperty(testInfoProperty3, struct{ a int }{1}) assert.Equal(t, "foo.bar.silent: test {prop3: {1}, prop2: hello world}", err.Error()) }) t.Run("NonPrintableIsInvisible", func(t *testing.T) { err := err.WithProperty(testProperty0, "nah") assert.Equal(t, "foo.bar.silent: test {prop2: hello world}", err.Error()) }) t.Run("WithUnderlying", func(t *testing.T) { err := err.WithUnderlyingErrors(testTypeSilent.New("underlying")) assert.Equal(t, "foo.bar.silent: test {prop2: hello world} (hidden: foo.bar.silent: underlying)", err.Error()) }) err2 := Decorate(err, "oops") t.Run("Decorate", func(t *testing.T) { assert.Equal(t, "oops, cause: foo.bar.silent: test {prop2: hello world}", err2.Error()) }) t.Run("DecorateAndAddMore", func(t *testing.T) { err := err2.WithProperty(testInfoProperty3, struct{ a int }{1}) assert.Equal(t, "oops {prop3: {1}}, cause: foo.bar.silent: test {prop2: hello world}", err.Error()) }) t.Run("DecorateAndAddSame", func(t *testing.T) { err := err2.WithProperty(testInfoProperty2, "cruel world") assert.Equal(t, "oops {prop2: cruel world}, cause: foo.bar.silent: test {prop2: hello world}", err.Error()) }) } func BenchmarkAllocProperty(b *testing.B) { const N = 9 var properties = []Property{} for j := 0; j < N; j++ { n := fmt.Sprintf("props%d", j) properties = append(properties, RegisterProperty(n)) b.Run(n, func(b *testing.B) { for k := 0; k < b.N; k++ { err := testTypeSilent.New("test") for i := 0; i < j; i++ { err = err.WithProperty(properties[i], 42) } } }) } } var sum int func BenchmarkGetProperty(b *testing.B) { const N = 9 var properties = []Property{} for j := 0; j < N; j++ { n := fmt.Sprintf("props%d", j) properties = append(properties, RegisterProperty(n)) b.Run(n, func(b *testing.B) { err := testTypeSilent.New("test") for i := 0; i < j; i++ { err = err.WithProperty(properties[i], 42) } for k := 0; k < b.N; k++ { v, ok := err.Property(testProperty0) if ok { sum += v.(int) } v, ok = err.Property(properties[j]) if ok { sum += v.(int) } v, ok = err.Property(properties[0]) if ok { sum += v.(int) } } }) } } errorx-1.2.0/readme.go000066400000000000000000000030311471340372300146200ustar00rootroot00000000000000// Package errorx provides error implementation and error-related utilities. // // Conventional approach towards errors in Go is quite limited. // The typical case implies an error being created at some point: // // return errors.New("now this is unfortunate") // // Then being passed along with a no-brainer: // // if err != nil { // return err // } // // And, finally, handled by printing it to the log file: // // log.Errorf("Error: %s", err) // // This approach is simple, but quite often it is not enough. // There is a need to add context information to error, to check or hide its properties. // If all else fails, it pays to have a stack trace printed along with error text. // // Syntax // // The code above could be modified in this fashion: // // return errorx.IllegalState.New("unfortunate") // // if err != nil { // return errorx.Decorate(err, "this could be so much better") // } // // log.Errorf("Error: %+v", err) // // Here errorx.Decorate is used to add more information, // and syntax like errorx.IsOfType can still be used to check the original error. // This error also holds a stack trace captured at the point of creation. // With errorx syntax, any of this may be customized: stack trace can be omitted, error type can be hidden. // Type can be further customized with Traits, and error with Properties. // Package provides utility functions to compose, switch over, check, and ignore errors based on their types and properties. // // See documentation for Error, Type and Namespace for more details. package errorx errorx-1.2.0/registry.go000066400000000000000000000033121471340372300152350ustar00rootroot00000000000000package errorx import "sync" // TypeSubscriber is an interface to receive callbacks on the registered error namespaces and types. // This may be used to create a user-defined registry, for example, to check if all type names are unique. // ISSUE: if .ApplyModifiers is called for a type/namespace, callback still receives a value without those modifiers. type TypeSubscriber interface { // OnNamespaceCreated is called exactly once for each namespace OnNamespaceCreated(namespace Namespace) // OnTypeCreated is called exactly once for each type OnTypeCreated(t *Type) } // RegisterTypeSubscriber adds a new TypeSubscriber. // A subscriber is guaranteed to receive callbacks for all namespaces and types. // If a type is already registered at the moment of subscription, a callback for this type is called immediately. func RegisterTypeSubscriber(s TypeSubscriber) { globalRegistry.registerTypeSubscriber(s) } type registry struct { mu sync.Mutex subscribers []TypeSubscriber knownNamespaces []Namespace knownTypes []*Type } var globalRegistry = ®istry{} func (r *registry) registerNamespace(namespace Namespace) { r.mu.Lock() defer r.mu.Unlock() r.knownNamespaces = append(r.knownNamespaces, namespace) for _, s := range r.subscribers { s.OnNamespaceCreated(namespace) } } func (r *registry) registerType(t *Type) { r.mu.Lock() defer r.mu.Unlock() r.knownTypes = append(r.knownTypes, t) for _, s := range r.subscribers { s.OnTypeCreated(t) } } func (r *registry) registerTypeSubscriber(s TypeSubscriber) { for _, ns := range r.knownNamespaces { s.OnNamespaceCreated(ns) } for _, t := range r.knownTypes { s.OnTypeCreated(t) } r.subscribers = append(r.subscribers, s) } errorx-1.2.0/registry_test.go000066400000000000000000000013101471340372300162700ustar00rootroot00000000000000package errorx import ( "testing" "github.com/stretchr/testify/require" ) func TestRegistry(t *testing.T) { s := &testSubscriber{} RegisterTypeSubscriber(s) require.Contains(t, s.namespaces, CommonErrors.Key()) require.Contains(t, s.types, AssertionFailed) ns := NewNamespace("TestRegistry") require.Contains(t, s.namespaces, ns.Key()) errorType := ns.NewType("Test") require.Contains(t, s.types, errorType) } type testSubscriber struct { types []*Type namespaces []NamespaceKey } func (s *testSubscriber) OnNamespaceCreated(namespace Namespace) { s.namespaces = append(s.namespaces, namespace.Key()) } func (s *testSubscriber) OnTypeCreated(t *Type) { s.types = append(s.types, t) } errorx-1.2.0/stackframe.go000066400000000000000000000014001471340372300155010ustar00rootroot00000000000000package errorx import ( "runtime" ) type frame interface { Function() string File() string Line() int } type frameHelper struct { } var frameHelperSingleton = &frameHelper{} type defaultFrame struct { frame *runtime.Frame } func (f *defaultFrame) Function() string { return f.frame.Function } func (f *defaultFrame) File() string { return f.frame.File } func (f *defaultFrame) Line() int { return f.frame.Line } func (c *frameHelper) GetFrames(pcs []uintptr) []frame { frames := runtime.CallersFrames(pcs[:]) result := make([]frame, 0, len(pcs)) var rawFrame runtime.Frame next := true for next { rawFrame, next = frames.Next() frameCopy := rawFrame frame := &defaultFrame{&frameCopy} result = append(result, frame) } return result } errorx-1.2.0/stacktrace.go000066400000000000000000000071011471340372300155110ustar00rootroot00000000000000package errorx import ( "fmt" "io" "runtime" "strconv" "sync" "sync/atomic" ) // StackTraceFilePathTransformer is a used defined transformer for file path in stack trace output. type StackTraceFilePathTransformer func(string) string // InitializeStackTraceTransformer provides a transformer to be used in formatting of all the errors. // It is OK to leave it alone, stack trace will retain its exact original information. // This feature may be beneficial, however, if a shortening of file path will make it more convenient to use. // One of such examples is to transform a project-related path from absolute to relative and thus more IDE-friendly. // // NB: error is returned if a transformer was already registered. // Transformer is changed nonetheless, the old one is returned along with an error. // User is at liberty to either ignore it, panic, reinstate the old transformer etc. func InitializeStackTraceTransformer(transformer StackTraceFilePathTransformer) (StackTraceFilePathTransformer, error) { stackTraceTransformer.mu.Lock() defer stackTraceTransformer.mu.Unlock() old := stackTraceTransformer.transform.Load().(StackTraceFilePathTransformer) stackTraceTransformer.transform.Store(transformer) if stackTraceTransformer.initialized { return old, InitializationFailed.New("stack trace transformer was already set up: %#v", old) } stackTraceTransformer.initialized = true return nil, nil } var stackTraceTransformer = struct { mu *sync.Mutex transform *atomic.Value initialized bool }{ &sync.Mutex{}, &atomic.Value{}, false, } func init() { stackTraceTransformer.transform.Store(transformStackTraceLineNoop) } var transformStackTraceLineNoop StackTraceFilePathTransformer = func(line string) string { return line } const ( stackTraceDepth = 128 // tuned so that in all control paths of error creation the first frame is useful // that is, the frame where New/Wrap/Decorate etc. are called; see TestStackTraceStart skippedFrames = 6 ) func collectStackTrace() *stackTrace { var pc [stackTraceDepth]uintptr depth := runtime.Callers(skippedFrames, pc[:]) return &stackTrace{ pc: pc[:depth], } } type stackTrace struct { pc []uintptr causeStackTrace *stackTrace } func (st *stackTrace) enhanceWithCause(causeStackTrace *stackTrace) { st.causeStackTrace = causeStackTrace } func (st *stackTrace) Format(s fmt.State, verb rune) { if st == nil { return } switch verb { case 'v', 's': st.formatStackTrace(s) if st.causeStackTrace != nil { io.WriteString(s, "\n ---------------------------------- ") st.causeStackTrace.Format(s, verb) } } } func (st *stackTrace) formatStackTrace(s fmt.State) { transformLine := stackTraceTransformer.transform.Load().(StackTraceFilePathTransformer) pc, cropped := st.deduplicateFramesWithCause() if len(pc) == 0 { return } frames := frameHelperSingleton.GetFrames(pc) for _, frame := range frames { io.WriteString(s, "\n at ") io.WriteString(s, frame.Function()) io.WriteString(s, "()\n\t") io.WriteString(s, transformLine(frame.File())) io.WriteString(s, ":") io.WriteString(s, strconv.Itoa(frame.Line())) } if cropped > 0 { io.WriteString(s, "\n ...\n (") io.WriteString(s, strconv.Itoa(cropped)) io.WriteString(s, " duplicated frames)") } } func (st *stackTrace) deduplicateFramesWithCause() ([]uintptr, int) { if st.causeStackTrace == nil { return st.pc, 0 } pc := st.pc causePC := st.causeStackTrace.pc for i := 1; i <= len(pc) && i <= len(causePC); i++ { if pc[len(pc)-i] != causePC[len(causePC)-i] { return pc[:len(pc)-i], i - 1 } } return nil, len(pc) } errorx-1.2.0/stacktrace_test.go000066400000000000000000000144771471340372300165660ustar00rootroot00000000000000package errorx import ( "bufio" "bytes" "errors" "fmt" "io" "strings" "testing" "github.com/stretchr/testify/require" ) func TestStackTraceStart(t *testing.T) { t.Run("New", func(t *testing.T) { err := AssertionFailed.New("achtung") output := fmt.Sprintf("%+v", err) require.Contains(t, output, "achtung", output) require.NotContains(t, output, "New()", output) require.Contains(t, output, "TestStackTraceStart", output) }) t.Run("NewWithNoMessage", func(t *testing.T) { err := AssertionFailed.NewWithNoMessage() output := fmt.Sprintf("%+v", err) require.NotContains(t, output, "NewWithNoMessage()", output) require.Contains(t, output, "TestStackTraceStart", output) }) t.Run("Wrap", func(t *testing.T) { err := AssertionFailed.Wrap(TimeoutElapsed.NewWithNoMessage(), "achtung") output := fmt.Sprintf("%+v", err) require.Contains(t, output, "achtung", output) require.NotContains(t, output, "Wrap()", output) require.Contains(t, output, "TestStackTraceStart", output) }) t.Run("WrapWithNoMessage", func(t *testing.T) { err := AssertionFailed.WrapWithNoMessage(TimeoutElapsed.NewWithNoMessage()) output := fmt.Sprintf("%+v", err) require.NotContains(t, output, "WrapWithNoMessage()", output) require.Contains(t, output, "TestStackTraceStart", output) }) t.Run("WrapAddStackTrace", func(t *testing.T) { err := testTypeSilent.NewWithNoMessage() output := fmt.Sprintf("%+v", err) require.NotContains(t, output, "TestStackTraceStart", output) err = AssertionFailed.Wrap(err, "achtung") output = fmt.Sprintf("%+v", err) require.Contains(t, output, "achtung", output) require.NotContains(t, output, "Wrap()", output) require.Contains(t, output, "TestStackTraceStart", output) }) t.Run("EnhanceStackTrace", func(t *testing.T) { err := EnhanceStackTrace(AssertionFailed.New("achtung"), "enhance") output := fmt.Sprintf("%+v", err) require.Contains(t, output, "achtung", output) require.NotContains(t, output, "EnhanceStackTrace()", output) require.Contains(t, output, "TestStackTraceStart", output) }) t.Run("EnhanceStackTraceWithRaw", func(t *testing.T) { err := EnhanceStackTrace(errors.New("achtung"), "enhance") output := fmt.Sprintf("%+v", err) require.Contains(t, output, "achtung", output) require.NotContains(t, output, "EnhanceStackTrace()", output) require.Contains(t, output, "TestStackTraceStart", output) }) t.Run("Decorate", func(t *testing.T) { err := Decorate(AssertionFailed.New("achtung"), "enhance") output := fmt.Sprintf("%+v", err) require.Contains(t, output, "achtung", output) require.NotContains(t, output, "Decorate()", output) require.Contains(t, output, "TestStackTraceStart", output) }) t.Run("DecorateWithRaw", func(t *testing.T) { err := Decorate(errors.New("achtung"), "enhance") output := fmt.Sprintf("%+v", err) require.Contains(t, output, "achtung", output) require.NotContains(t, output, "Decorate()", output) require.Contains(t, output, "TestStackTraceStart", output) }) t.Run("Raw", func(t *testing.T) { err := EnsureStackTrace(errors.New("achtung")) output := fmt.Sprintf("%+v", err) require.Contains(t, output, "achtung", output) require.NotContains(t, output, "EnsureStackTrace()", output) require.Contains(t, output, "TestStackTraceStart", output) }) } func TestStackTraceEnhance(t *testing.T) { err := stackTestStart() output := fmt.Sprintf("%+v", err) expected := map[string]int{ "TestStackTraceEnhance()": 0, "stackTestStart()": 0, "stackTestWithChan()": 0, "stackTest2()": 0, } checkStackTrace(t, output, expected) } func stackTestStart() error { ch := make(chan error) go stackTestWithChan(ch) return EnhanceStackTrace(<-ch, "") } func stackTestWithChan(ch chan error) { err := stackTest0() ch <- err } func stackTest0() error { return stackTest1() } func stackTest1() error { return stackTest2() } func stackTest2() error { return AssertionFailed.New("here be dragons") } func TestStackTraceDuplicate(t *testing.T) { err := stackTestDuplicate1() output := fmt.Sprintf("%+v", err) expected := map[string]int{ "TestStackTraceDuplicate()": 0, "stackTestDuplicate1()": 0, "stackTestStart1()": 0, "stackTestWithChan1()": 0, "stackTest21()": 0, } checkStackTrace(t, output, expected) } func stackTestDuplicate1() error { return EnhanceStackTrace(stackTestStart1(), "") } func stackTestStart1() error { ch := make(chan error) go stackTestWithChan1(ch) return EnhanceStackTrace(<-ch, "") } func stackTestWithChan1(ch chan error) { err := stackTest11() ch <- err } func stackTest11() error { return stackTest21() } func stackTest21() error { return AssertionFailed.New("here be dragons") } func TestStackTraceDuplicateWithIntermittentFrames(t *testing.T) { err := stackTestDuplicate2() output := fmt.Sprintf("%+v", err) expected := map[string]int{ "TestStackTraceDuplicateWithIntermittentFrames()": 0, "stackTestDuplicate2()": 0, "stackTestStart2()": 0, "enhanceFunc2()": 0, "stackTestWithChan2()": 0, "stackTest22()": 0, } checkStackTrace(t, output, expected) } func stackTestDuplicate2() error { err := stackTestStart2() return enhanceFunc2(err) } func enhanceFunc2(err error) error { return EnhanceStackTrace(err, "") } func stackTestStart2() error { ch := make(chan error) go stackTestWithChan2(ch) err := <-ch return EnhanceStackTrace(err, "") } func stackTestWithChan2(ch chan error) { err := stackTest12() ch <- err } func stackTest12() error { return stackTest22() } func stackTest22() error { return AssertionFailed.New("here be dragons and dungeons, too") } func checkStackTrace(t *testing.T, output string, expected map[string]int) { readByLine(t, output, func(line string) { for key := range expected { if strings.HasSuffix(line, key) { expected[key]++ } } }) for key, value := range expected { require.EqualValues(t, 1, value, "Wrong count (%d) of '%s' in:\n%s", value, key, output) } } func readByLine(t *testing.T, output string, f func(string)) { reader := bufio.NewReader(bytes.NewReader([]byte(output))) for { lineBytes, _, readErr := reader.ReadLine() if readErr == io.EOF { break } require.NoError(t, readErr) f(string(lineBytes)) } } errorx-1.2.0/switch.go000066400000000000000000000042371471340372300146750ustar00rootroot00000000000000package errorx // NotRecognisedType is a synthetic type used in TypeSwitch, signifying a presence of non-nil error of some other type. func NotRecognisedType() *Type { return notRecognisedType } // CaseNoError is a synthetic trait used in TraitSwitch, signifying an absence of error. func CaseNoError() Trait { return caseNoError } // CaseNoTrait is a synthetic trait used in TraitSwitch, signifying a presence of non-nil error that lacks specified traits. func CaseNoTrait() Trait { return caseNoTrait } var ( notRecognisedType = syntheticErrors.NewType("non.recognised") caseNoError = RegisterTrait("synthetic.no.error") caseNoTrait = RegisterTrait("synthetic.no.trait") ) // TypeSwitch is used to perform a switch around the type of an error. // For nil errors, returns nil. // For error types not in the 'types' list, including non-errorx errors, NotRecognisedType() is returned. // It is safe to treat NotRecognisedType() as 'any other type of not-nil error' case. // The effect is equivalent to a series of IsOfType() checks. // // NB: if more than one provided types matches the error, the first match in the providers list is recognised. func TypeSwitch(err error, types ...*Type) *Type { typed := Cast(err) switch { case err == nil: return nil case typed == nil: return NotRecognisedType() default: for _, t := range types { if typed.IsOfType(t) { return t } } return NotRecognisedType() } } // TraitSwitch is used to perform a switch around the trait of an error. // For nil errors, returns CaseNoError(). // For error types that lack any of the provided traits, including non-errorx errors, CaseNoTrait() is returned. // It is safe to treat CaseNoTrait() as 'any other kind of not-nil error' case. // The effect is equivalent to a series of HasTrait() checks. // // NB: if more than one provided types matches the error, the first match in the providers list is recognised. func TraitSwitch(err error, traits ...Trait) Trait { typed := Cast(err) switch { case err == nil: return CaseNoError() case typed == nil: return CaseNoTrait() default: for _, t := range traits { if typed.HasTrait(t) { return t } } return CaseNoTrait() } } errorx-1.2.0/switch_test.go000066400000000000000000000117151471340372300157330ustar00rootroot00000000000000package errorx import ( "fmt" "testing" "github.com/stretchr/testify/require" ) func TestErrorSwitch(t *testing.T) { t.Run("Simple", func(t *testing.T) { switch testType.NewWithNoMessage().Type() { case testType: // OK default: require.Fail(t, "") } }) t.Run("Wrapped", func(t *testing.T) { err := testTypeBar1.Wrap(testType.NewWithNoMessage(), "a") require.Nil(t, Ignore(err, testTypeBar1)) require.NotNil(t, Ignore(err, testType)) switch TypeSwitch(err, testType, testTypeBar1) { case testType: require.Fail(t, "") case testTypeBar1: // OK case nil: require.Fail(t, "") default: require.Fail(t, "") } }) t.Run("Raw", func(t *testing.T) { switch TypeSwitch(fmt.Errorf("test non-errorx error"), testType, testTypeBar1) { case testType: require.Fail(t, "") case testTypeBar1: require.Fail(t, "") case nil: require.Fail(t, "") default: // OK } }) t.Run("Nil", func(t *testing.T) { switch TypeSwitch(nil, testType, testTypeBar1) { case testType: require.Fail(t, "") case testTypeBar1: require.Fail(t, "") case nil: // OK default: require.Fail(t, "") } }) t.Run("Supertype", func(t *testing.T) { switch TypeSwitch(Decorate(testSubtype0.New("b"), "c"), testType, testTypeBar1) { case testTypeBar1: require.Fail(t, "") case testType: // OK case nil: require.Fail(t, "") default: require.Fail(t, "") } }) t.Run("Subtype", func(t *testing.T) { switch TypeSwitch(Decorate(testSubtype0.New("b"), "c"), testSubtype0, testTypeBar1) { case testTypeBar1: require.Fail(t, "") case testType: require.Fail(t, "") case testSubtype0: // OK case nil: require.Fail(t, "") default: require.Fail(t, "") } }) t.Run("SubSubtype", func(t *testing.T) { switch TypeSwitch(Decorate(testSubtype0.New("b"), "c"), testSubtype1, testTypeBar1) { case testTypeBar1: require.Fail(t, "") case testType: require.Fail(t, "") case testSubtype0: require.Fail(t, "") case testSubtype1: require.Fail(t, "") case nil: require.Fail(t, "") default: // OK } }) t.Run("Ordering", func(t *testing.T) { switch TypeSwitch(Decorate(testSubtype0.New("b"), "c"), testSubtype1, testType, testSubtype0, testTypeBar1) { case testTypeBar1: require.Fail(t, "") case testType: // OK case testSubtype0: require.Fail(t, "") case testSubtype1: require.Fail(t, "") case nil: require.Fail(t, "") default: require.Fail(t, "") } }) } func TestErrorSwitchUnrecognised(t *testing.T) { t.Run("Mismatch", func(t *testing.T) { switch TypeSwitch(Decorate(testTypeBar2.New("b"), "c"), testTypeBar1) { case testTypeBar1: require.Fail(t, "") case nil: require.Fail(t, "") case NotRecognisedType(): // OK default: require.Fail(t, "") } }) t.Run("Raw", func(t *testing.T) { switch TypeSwitch(fmt.Errorf("test"), testTypeBar1) { case testType: require.Fail(t, "") case nil: require.Fail(t, "") case NotRecognisedType(): // OK default: require.Fail(t, "") } }) t.Run("Nil", func(t *testing.T) { switch TypeSwitch(nil, testTypeBar1) { case testType: require.Fail(t, "") case nil: // OK case NotRecognisedType(): require.Fail(t, "") default: require.Fail(t, "") } }) } func TestErrorTraitSwitch(t *testing.T) { err := traitTestTimeoutError.Wrap(traitTestError3.NewWithNoMessage(), "a") require.True(t, HasTrait(err, Timeout())) require.False(t, HasTrait(err, testTrait0)) t.Run("Wrapped", func(t *testing.T) { switch TraitSwitch(err, Timeout(), testTrait0) { case testTrait0: require.Fail(t, "") case Timeout(): // OK case CaseNoError(): require.Fail(t, "") case CaseNoTrait(): require.Fail(t, "") default: require.Fail(t, "") } }) t.Run("Raw", func(t *testing.T) { switch TraitSwitch(fmt.Errorf("test non-errorx error"), Timeout(), testTrait0) { case testTrait0: require.Fail(t, "") case Timeout(): require.Fail(t, "") case CaseNoError(): require.Fail(t, "") case CaseNoTrait(): // OK default: require.Fail(t, "") } }) t.Run("Nil", func(t *testing.T) { switch TraitSwitch(nil, Timeout(), testTrait0) { case testTrait0: require.Fail(t, "") case Timeout(): require.Fail(t, "") case CaseNoError(): // OK case CaseNoTrait(): require.Fail(t, "") default: require.Fail(t, "") } }) t.Run("NoMatch", func(t *testing.T) { switch TraitSwitch(err, testTrait0) { case testTrait0: require.Fail(t, "") case Timeout(): require.Fail(t, "") case CaseNoError(): require.Fail(t, "") case CaseNoTrait(): // OK default: require.Fail(t, "") } }) t.Run("Ordering", func(t *testing.T) { switch TraitSwitch(traitTestTemporaryTimeoutError.Wrap(traitTestError3.NewWithNoMessage(), "a"), Temporary(), Timeout()) { case Timeout(): require.Fail(t, "") case Temporary(): // OK case CaseNoError(): require.Fail(t, "") case CaseNoTrait(): require.Fail(t, "") default: require.Fail(t, "") } }) } errorx-1.2.0/trait.go000066400000000000000000000042321471340372300145120ustar00rootroot00000000000000package errorx // Trait is a static characteristic of an error type. // All errors of a specific type possess exactly the same traits. // Traits are both defined along with an error and inherited from a supertype and a namespace. type Trait struct { id uint64 label string } // RegisterTrait declares a new distinct traits. // Traits are matched exactly, distinct traits are considered separate event if they have the same label. func RegisterTrait(label string) Trait { return newTrait(label) } // HasTrait checks if an error possesses the expected trait. // Traits are always properties of a type rather than of an instance, so trait check is an alternative to a type check. // This alternative is preferable, though, as it is less brittle and generally creates less of a dependency. func HasTrait(err error, key Trait) bool { typedErr := Cast(err) if typedErr == nil { return false } return typedErr.HasTrait(key) } // Temporary is a trait that signifies that an error is temporary in nature. func Temporary() Trait { return traitTemporary } // Timeout is a trait that signifies that an error is some sort iof timeout. func Timeout() Trait { return traitTimeout } // NotFound is a trait that marks such an error where the requested object is not found. func NotFound() Trait { return traitNotFound } // Duplicate is a trait that marks such an error where an update is failed as a duplicate. func Duplicate() Trait { return traitDuplicate } // IsTemporary checks for Temporary trait. func IsTemporary(err error) bool { return HasTrait(err, Temporary()) } // IsTimeout checks for Timeout trait. func IsTimeout(err error) bool { return HasTrait(err, Timeout()) } // IsNotFound checks for NotFound trait. func IsNotFound(err error) bool { return HasTrait(err, NotFound()) } // IsDuplicate checks for Duplicate trait. func IsDuplicate(err error) bool { return HasTrait(err, Duplicate()) } var ( traitTemporary = RegisterTrait("temporary") traitTimeout = RegisterTrait("timeout") traitNotFound = RegisterTrait("not_found") traitDuplicate = RegisterTrait("duplicate") ) func newTrait(label string) Trait { return Trait{ id: nextInternalID(), label: label, } } errorx-1.2.0/trait_test.go000066400000000000000000000050411471340372300155500ustar00rootroot00000000000000package errorx import ( "testing" "github.com/stretchr/testify/require" ) var ( testTrait0 = RegisterTrait("test0") testTrait1 = RegisterTrait("test1") testTrait2 = RegisterTrait("test2") traitTestNamespace = NewNamespace("traits") traitTestNamespace2 = NewNamespace("traits2", testTrait0) traitTestNamespace2Child = traitTestNamespace2.NewSubNamespace("child", testTrait1) traitTestError = traitTestNamespace.NewType("simple", testTrait1) traitTestError2 = traitTestNamespace2.NewType("simple", testTrait2) traitTestError3 = traitTestNamespace2Child.NewType("simple", testTrait2) traitTestTimeoutError = traitTestNamespace.NewType("timeout", Timeout()) traitTestTemporaryTimeoutError = traitTestTimeoutError.NewSubtype("temporary", Temporary()) ) func TestTrait(t *testing.T) { t.Run("Negative", func(t *testing.T) { err := traitTestError.New("test") require.False(t, IsTemporary(err)) }) t.Run("Positive", func(t *testing.T) { err := traitTestError.New("test") require.True(t, HasTrait(err, testTrait1)) }) t.Run("SubType", func(t *testing.T) { err := traitTestTimeoutError.New("test") require.True(t, IsTimeout(err)) require.False(t, IsTemporary(err)) err = traitTestTemporaryTimeoutError.New("test") require.True(t, IsTimeout(err)) require.True(t, IsTemporary(err)) }) t.Run("Wrap", func(t *testing.T) { err := traitTestTimeoutError.New("test") err = traitTestError2.Wrap(err, "") require.False(t, IsTimeout(err)) require.True(t, HasTrait(err, testTrait0)) require.False(t, HasTrait(err, testTrait1)) require.True(t, HasTrait(err, testTrait2)) }) t.Run("Decorate", func(t *testing.T) { err := traitTestTimeoutError.New("test") err = Decorate(err, "") require.True(t, IsTimeout(err)) require.False(t, IsTemporary(err)) }) } func TestTraitNamespace(t *testing.T) { t.Run("Negative", func(t *testing.T) { err := traitTestError.New("test") require.False(t, HasTrait(err, testTrait0)) require.True(t, HasTrait(err, testTrait1)) require.False(t, HasTrait(err, testTrait2)) }) t.Run("Inheritance", func(t *testing.T) { err := traitTestError2.New("test") require.True(t, HasTrait(err, testTrait0)) require.False(t, HasTrait(err, testTrait1)) require.True(t, HasTrait(err, testTrait2)) }) t.Run("DoubleInheritance", func(t *testing.T) { err := traitTestError3.New("test") require.True(t, HasTrait(err, testTrait0)) require.True(t, HasTrait(err, testTrait1)) require.True(t, HasTrait(err, testTrait2)) }) } errorx-1.2.0/type.go000066400000000000000000000132651471340372300143560ustar00rootroot00000000000000package errorx import ( "encoding" ) // Type is a distinct error type. // Belongs to a namespace, may be a descendant of another type in the same namespace. // May contain or inherit modifiers that alter the default properties for any error of this type. // May contain or inherit traits that all errors of this type will possess. type Type struct { namespace Namespace parent *Type id uint64 fullName string traits map[Trait]bool modifiers modifiers } var _ encoding.TextMarshaler = (*Type)(nil) // NewType defines a new distinct type within a namespace. func NewType(namespace Namespace, name string, traits ...Trait) *Type { return newType(namespace, nil, name, traits...) } // NewSubtype defines a new subtype within a namespace of a parent type. func (t *Type) NewSubtype(name string, traits ...Trait) *Type { return newType(t.namespace, t, name, traits...) } // ApplyModifiers makes a one-time modification of defaults in error creation. func (t *Type) ApplyModifiers(modifiers ...TypeModifier) *Type { t.modifiers = t.modifiers.ReplaceWith(newTypeModifiers(modifiers...)) return t } // New creates an error of this type with a message. // Without args, leaves the original message intact, so a message may be generated or provided externally. // With args, a formatting is performed, and it is therefore expected a format string to be constant. func (t *Type) New(message string, args ...interface{}) *Error { return NewErrorBuilder(t). WithConditionallyFormattedMessage(message, args...). Create() } // NewWithNoMessage creates an error of this type without any message. // May be used when other information is sufficient, such as error type and stack trace. func (t *Type) NewWithNoMessage() *Error { return NewErrorBuilder(t). Create() } // Wrap creates an error of this type with another as original cause. // As far as type checks are concerned, this error is the only one visible, with original present only in error message. // The original error will not pass its dynamic properties, and those are accessible only via direct walk over Cause() chain. // Without args, leaves the original message intact, so a message may be generated or provided externally. // With args, a formatting is performed, and it is therefore expected a format string to be constant. // NB: Wrap is NOT the reverse of errors.Unwrap() or Error.Unwrap() method; name may be changed in future releases to avoid confusion. func (t *Type) Wrap(err error, message string, args ...interface{}) *Error { return NewErrorBuilder(t). WithConditionallyFormattedMessage(message, args...). WithCause(err). Create() } // WrapWithNoMessage creates an error of this type with another as original cause and with no additional message. // May be used when other information is sufficient, such as error type, cause and its stack trace and message. // As far as type checks are concerned, this error is the only one visible, with original visible only in error message. // The original error will, however, pass its dynamic properties. func (t *Type) WrapWithNoMessage(err error) *Error { return NewErrorBuilder(t). WithCause(err). Create() } // IsOfType is a type check for error. // Returns true either if both are of exactly the same type, or if the same is true for one of current type's ancestors. func (t *Type) IsOfType(other *Type) bool { current := t for current != nil { if current.id == other.id { return true } current = current.parent } return false } // HasTrait checks if a type possesses the expected trait. func (t *Type) HasTrait(key Trait) bool { _, ok := t.traits[key] return ok } // IsOfType is a type check for errors. // Returns true either if both are of exactly the same type, or if the same is true for one of current type's ancestors. // Go 1.12 and below: for an error that does not have an errorx type, returns false. // 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. func IsOfType(err error, t *Type) bool { return isOfType(err, t) } // Supertype returns a parent type, if present. func (t *Type) Supertype() *Type { return t.parent } // FullName returns a fully qualified name if type, is not presumed to be unique, see TypeSubscriber. func (t *Type) FullName() string { return t.fullName } // Namespace returns a namespace this type belongs to. func (t *Type) Namespace() Namespace { return t.namespace } // RootNamespace returns a base namespace this type belongs to. func (t *Type) RootNamespace() Namespace { n := t.namespace for n.parent != nil { n = *n.parent } return n } func (t *Type) String() string { return t.FullName() } // MarshalText implements encoding.TextMarshaler func (t *Type) MarshalText() (text []byte, err error) { return []byte(t.String()), nil } func newType(namespace Namespace, parent *Type, name string, traits ...Trait) *Type { collectModifiers := func() modifiers { if parent == nil { return newInheritedModifiers(namespace.modifiers) } return newInheritedModifiers(parent.modifiers) } collectTraits := func() map[Trait]bool { result := make(map[Trait]bool) if parent != nil { for trait := range parent.traits { result[trait] = true } } for trait := range namespace.collectTraits() { result[trait] = true } for _, trait := range traits { result[trait] = true } return result } createFullName := func() string { if parent == nil { return namespace.FullName() + "." + name } return parent.FullName() + "." + name } t := &Type{ id: nextInternalID(), namespace: namespace, parent: parent, fullName: createFullName(), traits: collectTraits(), modifiers: collectModifiers(), } globalRegistry.registerType(t) return t } errorx-1.2.0/type_test.go000066400000000000000000000021231471340372300154040ustar00rootroot00000000000000package errorx import ( "errors" "testing" "github.com/stretchr/testify/require" ) func TestTypeName(t *testing.T) { require.Equal(t, "foo.bar", testType.FullName()) } func TestSubTypeName(t *testing.T) { require.Equal(t, "foo.bar.internal.wat", testSubtype1.FullName()) } func TestErrorTypeCheck(t *testing.T) { require.True(t, testSubtype1.IsOfType(testSubtype1)) require.False(t, testSubtype1.IsOfType(NewNamespace("a").NewType("b"))) } func TestErrorTypeCheckNonErrorx(t *testing.T) { require.False(t, IsOfType(errors.New("test"), testSubtype1)) } func TestErrorTypeUpCast(t *testing.T) { require.True(t, testSubtype1.IsOfType(testSubtype0)) require.True(t, testSubtype1.IsOfType(testType)) } func TestErrorTypeDownCast(t *testing.T) { require.False(t, testSubtype0.IsOfType(testSubtype1)) require.False(t, testType.IsOfType(testSubtype1)) } func TestErrorTypeSiblingsCast(t *testing.T) { subtype10 := testSubtype0.NewSubtype("wat!") subtype11 := testSubtype0.NewSubtype("oops") require.False(t, subtype10.IsOfType(subtype11)) require.False(t, subtype11.IsOfType(subtype10)) } errorx-1.2.0/utils.go000066400000000000000000000031521471340372300145270ustar00rootroot00000000000000package errorx // Cast attempts to cast an error to errorx Type, returns nil if cast has failed. func Cast(err error) *Error { if e, ok := err.(*Error); ok && e != nil { return e } return nil } // Ignore returns nil if an error is of one of the provided types, returns the provided error otherwise. // May be used if a particular error signifies a mark in control flow rather than an error to be reported to the caller. func Ignore(err error, types ...*Type) error { if e := Cast(err); e != nil { for _, t := range types { if e.IsOfType(t) { return nil } } } return err } // IgnoreWithTrait returns nil if an error has one of the provided traits, returns the provided error otherwise. // May be used if a particular error trait signifies a mark in control flow rather than an error to be reported to the caller. func IgnoreWithTrait(err error, traits ...Trait) error { if e := Cast(err); e != nil { for _, t := range traits { if e.HasTrait(t) { return nil } } } return err } // GetTypeName returns the full type name if an error; returns an empty string for non-errorx error. // For decorated errors, the type of an original cause is used. func GetTypeName(err error) string { if e := Cast(err); e != nil { t := e.Type() if t != foreignType { return t.FullName() } } return "" } // ReplicateError is a utility function to duplicate error N times. // May be handy do demultiplex a single original error to a number of callers/requests. func ReplicateError(err error, count int) []error { result := make([]error, count) for i := range result { result[i] = err } return result } errorx-1.2.0/utils_test.go000066400000000000000000000027031471340372300155670ustar00rootroot00000000000000package errorx import ( "errors" "testing" "github.com/stretchr/testify/require" ) func TestIgnoreWithTrait(t *testing.T) { t.Run("Empty", func(t *testing.T) { require.Error(t, IgnoreWithTrait(TimeoutElapsed.NewWithNoMessage())) }) t.Run("AnotherTrait", func(t *testing.T) { require.Error(t, IgnoreWithTrait(TimeoutElapsed.NewWithNoMessage(), NotFound())) }) t.Run("Positive", func(t *testing.T) { require.NoError(t, IgnoreWithTrait(TimeoutElapsed.NewWithNoMessage(), Timeout())) }) t.Run("OneOfMany", func(t *testing.T) { require.NoError(t, IgnoreWithTrait(TimeoutElapsed.NewWithNoMessage(), NotFound(), Timeout())) }) } func TestGetTypeName(t *testing.T) { t.Run("Simple", func(t *testing.T) { require.EqualValues(t, "common.assertion_failed", GetTypeName(AssertionFailed.NewWithNoMessage())) }) t.Run("Wrap", func(t *testing.T) { require.EqualValues(t, "common.illegal_state", GetTypeName(IllegalState.WrapWithNoMessage(AssertionFailed.NewWithNoMessage()))) }) t.Run("Decorate", func(t *testing.T) { require.EqualValues(t, "common.assertion_failed", GetTypeName(Decorate(AssertionFailed.NewWithNoMessage(), ""))) }) t.Run("Nil", func(t *testing.T) { require.EqualValues(t, "", GetTypeName(nil)) }) t.Run("Raw", func(t *testing.T) { require.EqualValues(t, "", GetTypeName(errors.New("test"))) }) t.Run("DecoratedRaw", func(t *testing.T) { require.EqualValues(t, "", GetTypeName(Decorate(errors.New("test"), ""))) }) } errorx-1.2.0/wrap.go000066400000000000000000000101061471340372300143350ustar00rootroot00000000000000package errorx var ( // Most errors from this namespace are made private in order to disallow and direct type checks in the user code syntheticErrors = NewNamespace("synthetic") // Private error type for non-errors errors, used as a not-nil substitute that cannot be type-checked directly foreignType = syntheticErrors.NewType("foreign") // Private error type used as a universal wrapper, meant to add nothing at all to the error apart from some message transparentWrapper = syntheticErrors.NewType("decorate").ApplyModifiers(TypeModifierTransparent) // Private error type used as a densely opaque wrapper which hides both the original error and its own type opaqueWrapper = syntheticErrors.NewType("wrap") // Private error type used for stack trace capture stackTraceWrapper = syntheticErrors.NewType("stacktrace").ApplyModifiers(TypeModifierTransparent) ) // Decorate allows to pass some text info along with a message, leaving its semantics totally intact. // Perceived type, traits and properties of the resulting error are those of the original. // Without args, leaves the provided message intact, so a message may be generated or provided externally. // With args, a formatting is performed, and it is therefore expected a format string to be constant. func Decorate(err error, message string, args ...interface{}) *Error { return NewErrorBuilder(transparentWrapper). WithConditionallyFormattedMessage(message, args...). WithCause(err). Create() } // EnhanceStackTrace has all the properties of the Decorate() method // and additionally extends the stack trace of the original error. // Designed to be used when a original error is passed from another goroutine rather than from a direct method call. // If, however, it is called in the same goroutine, formatter makes some moderated effort to remove duplication. func EnhanceStackTrace(err error, message string, args ...interface{}) *Error { return NewErrorBuilder(transparentWrapper). WithConditionallyFormattedMessage(message, args...). WithCause(err). EnhanceStackTrace(). Create() } // EnsureStackTrace is a utility to ensure the stack trace is captured in provided error. // If this is already true, it is returned unmodified. // Otherwise, it is decorated with stack trace. func EnsureStackTrace(err error) *Error { if typedErr := Cast(err); typedErr != nil && typedErr.stackTrace != nil { return typedErr } return NewErrorBuilder(stackTraceWrapper). WithConditionallyFormattedMessage(""). WithCause(err). EnhanceStackTrace(). Create() } // DecorateMany performs a transparent wrap of multiple errors with additional message. // If there are no errors, or all errors are nil, returns nil. // If all errors are of the same type (for example, if there is only one), wraps them transparently. // Otherwise, an opaque wrap is performed, that is, IsOfType checks will fail on underlying error types. func DecorateMany(message string, errs ...error) error { errs = ignoreEmpty(errs) if len(errs) == 0 { return nil } if !areAllOfTheSameType(errs...) { return WrapMany(opaqueWrapper, message, errs...) } return WrapMany(transparentWrapper, message, errs...) } // WrapMany is a utility to wrap multiple errors. // If there are no errors, or all errors are nil, returns nil. // Otherwise, the fist error is treated as an original cause, others are added as underlying. func WrapMany(errorType *Type, message string, errs ...error) error { errs = ignoreEmpty(errs) if len(errs) == 0 { return nil } cause := errs[0] suppressed := errs[1:] return errorType.Wrap(cause, message).WithUnderlyingErrors(suppressed...) } func ignoreEmpty(errs []error) []error { result := make([]error, 0, len(errs)) for _, err := range errs { if err != nil { result = append(result, err) } } return result } func areAllOfTheSameType(errs ...error) bool { if len(errs) < 2 { return true } var errorType *Type for _, err := range errs { typedError := Cast(err) if typedError == nil { return false } if errorType == nil { errorType = typedError.Type() } else if errorType != typedError.Type() { return false } } return true } errorx-1.2.0/wrap_test.go000066400000000000000000000042651471340372300154050ustar00rootroot00000000000000package errorx import ( "errors" "fmt" "testing" "github.com/stretchr/testify/require" ) func TestEnsureStackTrace(t *testing.T) { t.Run("Simple", func(t *testing.T) { err := EnsureStackTrace(testType.New("good")) require.True(t, IsOfType(err, testType)) output := fmt.Sprintf("%+v", err) require.Contains(t, output, "good", output) require.Contains(t, output, "TestEnsureStackTrace", output) }) t.Run("NoTrace", func(t *testing.T) { err := EnsureStackTrace(testTypeSilent.New("average")) require.True(t, IsOfType(err, testType)) output := fmt.Sprintf("%+v", err) require.Contains(t, output, "average", output) require.Contains(t, output, "TestEnsureStackTrace", output) }) t.Run("Raw", func(t *testing.T) { err := EnsureStackTrace(errors.New("bad")) output := fmt.Sprintf("%+v", err) require.Contains(t, output, "bad", output) require.Contains(t, output, "TestEnsureStackTrace", output) }) } func TestDecorateMany(t *testing.T) { t.Run("Single", func(t *testing.T) { err := DecorateMany("ouch!", testType.NewWithNoMessage()) require.Equal(t, "ouch!, cause: foo.bar", err.Error()) require.True(t, IsOfType(err, testType)) require.Equal(t, testType, err.(*Error).Type()) }) t.Run("SingleEmpty", func(t *testing.T) { require.Nil(t, DecorateMany("ouch!", nil)) }) t.Run("ManyEmpty", func(t *testing.T) { require.Nil(t, DecorateMany("ouch!", nil, nil)) require.Nil(t, DecorateMany("ouch!", nil, nil, nil)) }) t.Run("ManySame", func(t *testing.T) { err := DecorateMany("ouch!", testType.NewWithNoMessage(), nil, testType.New("bad")) require.Equal(t, "ouch!, cause: foo.bar (hidden: foo.bar: bad)", err.Error()) require.True(t, IsOfType(err, testType)) require.Equal(t, testType, err.(*Error).Type()) }) t.Run("ManyDifferent", func(t *testing.T) { err := DecorateMany("ouch!", testTypeBar1.NewWithNoMessage(), testTypeBar2.New("bad"), nil) require.Equal(t, "synthetic.wrap: ouch!, cause: foo.bar1 (hidden: foo.bar2: bad)", err.Error()) require.False(t, IsOfType(err, testTypeBar1)) require.False(t, IsOfType(err, testTypeBar2)) require.NotEqual(t, testTypeBar1, err.(*Error).Type()) require.NotEqual(t, testTypeBar2, err.(*Error).Type()) }) }