Giter Site home page Giter Site logo

errorx's Introduction

Github Actions 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.

errorx's People

Contributors

codyoss avatar funny-falcon avatar g7r avatar gavv avatar isopov avatar peterivanov avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

errorx's Issues

JSON formatting for an error

It would be, IMHO, reasonable to make a custom MarshalJSON() function to handle marshaling given Error with all printable properties and, potentially, stack trace to JSON. Maybe not all will need this, but a lot of folks are using some Web framework, and that might come in handy...

As I am happy to make the implementation, I have two open questions which need an answer to address a variety of needs, not the one I see at this point:

  1. As I assume the format can be something like:
    { 
        "msg": "error was here",
        "properties": {
            "propertyString": "value",
            "propertyInt": 123
        }
    }
    I am not sure if the stacked errors should appear as a loop under some property like cause and so on? Just thinking here. One more single cause property with a message might be also sufficient, but I am not certain which would be more useful here...
  2. As I am new to this package, am I missing something that should be handled by MarshalJSON on top of that? Just asking as I am really not sure if there is something I am missing otherwise...

A really quick and simple implementation can be as follows:

func (e *Error) MarshalJSON() ([]byte, error) {
	return json.Marshal(&struct {
		Message    string                 `json:"message"`
		Properties map[string]interface{} `json:"properties,omitempty"`
	}{
		Message: e.message,
		Properties: e.mapFromPrintableProperties(),
	})
}

func (e *Error) mapFromPrintableProperties() map[string]interface{} {
	uniq := make(map[string]interface{}, e.printablePropertyCount)
	for m := e.properties; m != nil; m = m.next {
		if !m.p.printable {
			continue
		}
		if _, ok := uniq[m.p.label]; ok {
			continue
		}
		uniq[m.p.label] = m.value
	}
	return uniq
}

Happy to make a PR if that is acceptable for a larger audience

Master is not released, but used in redispipe

Hi!

You didn't set any tag for new function errorx.RegisterPrintableProperty and use it in github.com/joomcode/redispipe.

Such behaviour breaks usage of go modules in go1.11+

cd `mktemp -d`
go mod init example.com/test
cat <<EOD > main.go
package main

import (
        _ "github.com/joomcode/redispipe/redis"
)

func main() {}

EOD
go build .

will return

go: finding github.com/joomcode/redispipe/redis latest
# github.com/joomcode/redispipe/redis
/Users/ikorolev/.gvm/pkgsets/go1.11.4/global/pkg/mod/github.com/joomcode/[email protected]/redis/error.go:88:11: undefined: errorx.RegisterPrintableProperty
/Users/ikorolev/.gvm/pkgsets/go1.11.4/global/pkg/mod/github.com/joomcode/[email protected]/redis/error.go:90:10: undefined: errorx.RegisterPrintableProperty
/Users/ikorolev/.gvm/pkgsets/go1.11.4/global/pkg/mod/github.com/joomcode/[email protected]/redis/error.go:92:13: undefined: errorx.RegisterPrintableProperty
/Users/ikorolev/.gvm/pkgsets/go1.11.4/global/pkg/mod/github.com/joomcode/[email protected]/redis/error.go:94:14: undefined: errorx.RegisterPrintableProperty
/Users/ikorolev/.gvm/pkgsets/go1.11.4/global/pkg/mod/github.com/joomcode/[email protected]/redis/error.go:96:15: undefined: errorx.RegisterPrintableProperty
/Users/ikorolev/.gvm/pkgsets/go1.11.4/global/pkg/mod/github.com/joomcode/[email protected]/redis/error.go:100:14: undefined: errorx.RegisterPrintableProperty

This happens because go finds the latest semver tags and put it to go.mod:

cat go.mod
module example.com/test

require (
	github.com/joomcode/errorx v0.1.0 // indirect
	github.com/joomcode/redispipe v0.9.0
)

Could you please make a new release of this lib? That would be enough.

Structured stack trace

How i can extract stack trace from this library Error type like at https://github.com/pkg/errors library?

type stackTracer interface {
        StackTrace() errors.StackTrace
}
err.(stackTracer).StackTrace() // get structured stack trace

If use something fmt.Errorf("%+v", err), but return only text. I have use this library, but cant look good stack trace in sentry and cant write converter/adapter for Error type.

Errorx doesn't fully respect wrap semantic

There are a number of errorx functions that rely on Cast implementation. Cast was designed in pre Go 1.13 times and it doesn't respect wrap semantic of Go 1.13. While I'm not sure whether Cast should respect it, but I'm pretty sure that functions like HasTrait, Ignore, IgnoreWithTrait, ExtractProperty and probably TraitSwitch and TypeSwitch should.

There are more places using Cast:

  1. (ErrorBuilder).WithCause(error), (ErrorBuilder).EnhanceStackTrace() and (ErrorBuilder).assembleStackTrace(). Should it know about wrap? Probably yes.
  2. (*Error).Is(target error). Should we accept that target could be a wrapped *errorx.Error. I am personally not sure here. Standard library implementations don't unwrap target which is an argument to stick to current behaviour.
  3. (*Error).Property(Property). Should it work when underlying property is buried under non-errorx wrapper? Looks like it should. But I then see that Property being a method is a wrong abstraction. Should we hide the method in favor to ExtractProperty?
  4. errorx.GetTypeName(error). I'm not sure what would be the least surprising behaviour but the following example definitely looks broken: https://play.golang.org/p/_UAGbNO2ZlH

There also is a errorx.WithPayload which accepts *errorx.Error as an argument. To call this function the client code have to cast error to *errorx.Error with something like errorx.Cast. Should we change errorx.WithPayload to accept error?

Also there are a lot of Cast usages in our private codebase that will benefit from Cast being wrap-aware.

Whooa, that was a lot of concerns. Should I split them to separate issues? Maybe, but let's discuss them first.

Decorate misbehave if err==nil

What happens? errorx.Decorate returns an error with the following properties:

  • e.IsOfType(...) always returns false
  • e.Type() returns synthetic.foreign

I suggest that errorx.Decorate(nil, "") should either return nil or panic.

Improve Documentation

I am trying to use your package for certain use cases and i am confused beyond a point. For instance, i have scenarios for custom errors and error types or error codes etc. I am looking at adding namespaces etc and your documentation is not helping me move forward. Could someone please address it and help me or developers like me understand how to go about using your library better?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.