errorx

package module
Version: v1.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 19, 2020 License: MIT Imports: 9 Imported by: 94

README

Build Status GoDoc Report Card gocover.io Mentioned in Awesome Go

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:

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.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:

return errorx.IllegalState.New("unfortunate")
if err != nil {
  return errorx.Decorate(err, "this could be so much better")
}
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:

// 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:

// 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:

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:

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:

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:

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 for other errorx features:

  • Namespaces
  • Type switches
  • errorx.Ignore
  • Trait inheritance
  • Dynamic properties
  • Panic-related utils
  • Type registry
  • etc.

Documentation

Overview

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.

Index

Examples

Constants

This section is empty.

Variables

View Source
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")
)

Functions

func DecorateMany

func DecorateMany(message string, errs ...error) error

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.

Example
package main

import (
	"fmt"

	"github.com/joomcode/errorx"
)

func main() {
	err0 := someFunc()
	err1 := someFunc()
	err := errorx.DecorateMany("both calls failed", err0, err1)
	fmt.Println(err.Error())

}

func someFunc() error {
	return errorx.AssertionFailed.New("example")
}
Output:

both calls failed, cause: common.assertion_failed: example (hidden: common.assertion_failed: example)

func ErrorFromPanic

func ErrorFromPanic(recoverResult interface{}) (error, bool)

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 ExtractContext

func ExtractContext(err error) (context.Context, bool)

ExtractContext is a statically typed helper to extract a context property from an error.

func ExtractPayload

func ExtractPayload(err error) (interface{}, bool)

ExtractPayload is a helper to extract a payload property from an error.

func ExtractProperty

func ExtractProperty(err error, key Property) (interface{}, bool)

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 GetTypeName

func GetTypeName(err error) string

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 HasTrait

func HasTrait(err error, key Trait) bool

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 Ignore

func Ignore(err error, types ...*Type) error

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.

Example
package main

import (
	"fmt"

	"github.com/joomcode/errorx"
)

func main() {
	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
<nil>
more info, cause: common.illegal_argument

func IgnoreWithTrait

func IgnoreWithTrait(err error, traits ...Trait) error

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.

Example
package main

import (
	"fmt"

	"github.com/joomcode/errorx"
)

func main() {
	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
<nil>
more info, cause: common.timeout

func IsDuplicate

func IsDuplicate(err error) bool

IsDuplicate checks for Duplicate trait.

func IsNotFound

func IsNotFound(err error) bool

IsNotFound checks for NotFound trait.

func IsOfType

func IsOfType(err error, t *Type) bool

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. For an error that does not have an errorx type, returns false.

Example
package main

import (
	"fmt"

	"github.com/joomcode/errorx"
)

func main() {
	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 IsTemporary

func IsTemporary(err error) bool

IsTemporary checks for Temporary trait.

func IsTimeout

func IsTimeout(err error) bool

IsTimeout checks for Timeout trait.

func Panic

func Panic(err error) error

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 RegisterTypeSubscriber

func RegisterTypeSubscriber(s TypeSubscriber)

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 ReplicateError

func ReplicateError(err error, count int) []error

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 WrapMany

func WrapMany(errorType *Type, message string, errs ...error) error

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.

Types

type Error

type Error struct {
	// contains filtered or unexported fields
}

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.

func Cast

func Cast(err error) *Error

Cast attempts to cast an error to errorx Type, returns nil if cast has failed.

func Decorate

func Decorate(err error, message string, args ...interface{}) *Error

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.

Example
package main

import (
	"fmt"

	"github.com/joomcode/errorx"
)

func main() {
	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())

}

func someFunc() error {
	return errorx.AssertionFailed.New("example")
}
Output:

common.assertion_failed: example
decorate, cause: common.assertion_failed: example
outer decorate, cause: decorate, cause: common.assertion_failed: example

func EnhanceStackTrace

func EnhanceStackTrace(err error, message string, args ...interface{}) *Error

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.

Example
package main

import (
	"fmt"

	"github.com/joomcode/errorx"
)

func main() {
	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 nestedCall() error {
	return someFunc()
}

func someFunc() error {
	return errorx.AssertionFailed.New("example")
}
Output:

func EnsureStackTrace

func EnsureStackTrace(err error) *Error

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 WithContext

func WithContext(err *Error, ctx context.Context) *Error

WithContext is a statically typed helper to add a context property to an error.

func WithPayload

func WithPayload(err *Error, payload interface{}) *Error

WithPayload is a helper to add a payload property to an error.

func (*Error) Cause

func (e *Error) Cause() error

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 (*Error) Error

func (e *Error) Error() string

Error implements the error interface. A result is the same as with %s formatter and does not contain a stack trace.

func (*Error) Format

func (e *Error) Format(s fmt.State, verb rune)

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.

Example
package main

import (
	"fmt"

	"github.com/joomcode/errorx"
)

func main() {
	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 nestedCall() error {
	return someFunc()
}

func someFunc() error {
	return errorx.AssertionFailed.New("example")
}
Output:

func (*Error) HasTrait

func (e *Error) HasTrait(key Trait) bool

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 (*Error) IsOfType

func (e *Error) IsOfType(t *Type) bool

IsOfType is a proper type check for an error. It takes the transparency and error types hierarchy into account, so that type check against any supertype of the original cause passes.

func (*Error) Message

func (e *Error) Message() string

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 (*Error) Property

func (e *Error) Property(key Property) (interface{}, bool)

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 (*Error) Type

func (e *Error) Type() *Type

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 (*Error) WithProperty

func (e *Error) WithProperty(key Property, value interface{}) *Error

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 (*Error) WithUnderlyingErrors

func (e *Error) WithUnderlyingErrors(errs ...error) *Error

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.

Example
package main

import (
	"fmt"

	"github.com/joomcode/errorx"
)

func main() {
	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())
}

func getBodyAndError() ([]byte, error) {
	return nil, errorx.AssertionFailed.New("example")
}

func getDetailsFromBody(s []byte) (string, error) {
	return "", errorx.IllegalFormat.New(string(s))
}
Output:

common.assertion_failed: failed to read details, cause: common.assertion_failed: example (hidden: common.illegal_format)

type ErrorBuilder

type ErrorBuilder struct {
	// contains filtered or unexported fields
}

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.

func NewErrorBuilder

func NewErrorBuilder(t *Type) ErrorBuilder

NewErrorBuilder creates error builder from an existing error type.

func (ErrorBuilder) Create

func (eb ErrorBuilder) Create() *Error

Create returns an error with specified params.

func (ErrorBuilder) EnhanceStackTrace

func (eb ErrorBuilder) EnhanceStackTrace() ErrorBuilder

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 (ErrorBuilder) Transparent

func (eb ErrorBuilder) Transparent() ErrorBuilder

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 (ErrorBuilder) WithCause

func (eb ErrorBuilder) WithCause(err error) ErrorBuilder

WithCause provides an original cause for error. For non-errorx errors, a stack trace is collected. 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 (ErrorBuilder) WithConditionallyFormattedMessage

func (eb ErrorBuilder) WithConditionallyFormattedMessage(message string, args ...interface{}) ErrorBuilder

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.

type Namespace

type Namespace struct {
	// contains filtered or unexported fields
}

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

func NewNamespace

func NewNamespace(name string, traits ...Trait) Namespace

NewNamespace defines a namespace with a name and, optionally, a number of inheritable traits.

func (Namespace) ApplyModifiers

func (n Namespace) ApplyModifiers(modifiers ...TypeModifier) Namespace

ApplyModifiers makes a one-time modification of defaults in error creation.

func (Namespace) FullName

func (n Namespace) FullName() string

FullName returns a full name of a namespace.

func (Namespace) IsNamespaceOf

func (n Namespace) IsNamespaceOf(t *Type) bool

IsNamespaceOf checks whether or not an error belongs either to this namespace or some of its sub-namespaces.

func (Namespace) Key

func (n Namespace) Key() NamespaceKey

Key returns a comparison key for namespace.

func (Namespace) NewSubNamespace

func (n Namespace) NewSubNamespace(name string, traits ...Trait) Namespace

NewSubNamespace defines a child namespace that inherits all that is defined for a parent and, optionally, adds some more.

func (Namespace) NewType

func (n Namespace) NewType(typeName string, traits ...Trait) *Type

NewType creates a new type within a namespace that inherits all that is defined for namespace and, optionally, adds some more.

func (Namespace) Parent

func (n Namespace) Parent() *Namespace

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 (Namespace) String

func (n Namespace) String() string

type NamespaceKey

type NamespaceKey struct {
	// contains filtered or unexported fields
}

NamespaceKey is a comparable descriptor of a Namespace.

type Property

type Property struct {
	// contains filtered or unexported fields
}

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.

func PropertyContext

func PropertyContext() Property

PropertyContext is a context property, value is expected to be of context.Context type.

func PropertyPayload

func PropertyPayload() Property

PropertyPayload is a payload property, value may contain user defined structure with arbitrary data passed along with an error.

func RegisterPrintableProperty added in v0.8.0

func RegisterPrintableProperty(label string) Property

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 RegisterProperty

func RegisterProperty(label string) Property

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.

type StackTraceFilePathTransformer

type StackTraceFilePathTransformer func(string) string

StackTraceFilePathTransformer is a used defined transformer for file path in stack trace output.

func InitializeStackTraceTransformer

func InitializeStackTraceTransformer(transformer StackTraceFilePathTransformer) (StackTraceFilePathTransformer, error)

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.

type Trait

type Trait struct {
	// contains filtered or unexported fields
}

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.

func CaseNoError

func CaseNoError() Trait

CaseNoError is a synthetic trait used in TraitSwitch, signifying an absence of error.

func CaseNoTrait

func CaseNoTrait() Trait

CaseNoTrait is a synthetic trait used in TraitSwitch, signifying a presence of non-nil error that lacks specified traits.

func Duplicate

func Duplicate() Trait

Duplicate is a trait that marks such an error where an update is failed as a duplicate.

func NotFound

func NotFound() Trait

NotFound is a trait that marks such an error where the requested object is not found.

func RegisterTrait

func RegisterTrait(label string) Trait

RegisterTrait declares a new distinct traits. Traits are matched exactly, distinct traits are considered separate event if they have the same label.

func Temporary

func Temporary() Trait

Temporary is a trait that signifies that an error is temporary in nature.

func Timeout

func Timeout() Trait

Timeout is a trait that signifies that an error is some sort iof timeout.

func TraitSwitch

func TraitSwitch(err error, traits ...Trait) Trait

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.

Example
package main

import (
	"fmt"

	"github.com/joomcode/errorx"
)

func main() {
	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

type Type

type Type struct {
	// contains filtered or unexported fields
}

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.

func NewType

func NewType(namespace Namespace, name string, traits ...Trait) *Type

NewType defines a new distinct type within a namespace.

func NotRecognisedType

func NotRecognisedType() *Type

NotRecognisedType is a synthetic type used in TypeSwitch, signifying a presence of non-nil error of some other type.

func TypeSwitch

func TypeSwitch(err error, types ...*Type) *Type

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.

Example
package main

import (
	"fmt"

	"github.com/joomcode/errorx"
)

func main() {
	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 (*Type) ApplyModifiers

func (t *Type) ApplyModifiers(modifiers ...TypeModifier) *Type

ApplyModifiers makes a one-time modification of defaults in error creation.

func (*Type) FullName

func (t *Type) FullName() string

FullName returns a fully qualified name if type, is not presumed to be unique, see TypeSubscriber.

func (*Type) HasTrait added in v0.8.0

func (t *Type) HasTrait(key Trait) bool

HasTrait checks if a type possesses the expected trait.

func (*Type) IsOfType

func (t *Type) IsOfType(other *Type) bool

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 (*Type) MarshalText

func (t *Type) MarshalText() (text []byte, err error)

MarshalText implements encoding.TextMarshaler

func (*Type) Namespace added in v1.0.2

func (t *Type) Namespace() Namespace

Namespace returns a namespace this type belongs to.

func (*Type) New

func (t *Type) New(message string, args ...interface{}) *Error

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 (*Type) NewSubtype

func (t *Type) NewSubtype(name string, traits ...Trait) *Type

NewSubtype defines a new subtype within a namespace of a parent type.

func (*Type) NewWithNoMessage

func (t *Type) NewWithNoMessage() *Error

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 (*Type) RootNamespace

func (t *Type) RootNamespace() Namespace

RootNamespace returns a base namespace this type belongs to.

func (*Type) String

func (t *Type) String() string

func (*Type) Supertype

func (t *Type) Supertype() *Type

Supertype returns a parent type, if present.

func (*Type) Wrap

func (t *Type) Wrap(err error, message string, args ...interface{}) *Error

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.

Example
package main

import (
	"fmt"

	"github.com/joomcode/errorx"
)

func main() {
	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 (*Type) WrapWithNoMessage

func (t *Type) WrapWithNoMessage(err error) *Error

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.

type TypeModifier

type TypeModifier int

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.

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 TypeSubscriber

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)
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
t or T : Toggle theme light dark auto
y or Y : Canonical URL