A comprehensive error handling library for Go

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.
Comments
  • GetAllPrintableProperties will return a map of all printable properties

    GetAllPrintableProperties will return a map of all printable properties

    As discussed in #21, we can simply move some of the code and create a method corresponding to current logic to expose all properties from the error for further processing of errors in situations one would to use them in REST APIs and such.

    Simple code like the one below can cover creating a struct from errorx.Error:

    type ErrorJson struct {
    	Message    string                 `json:"message"`
    	Properties map[string]interface{} `json:"properties,omitempty"`
    	Cause      interface{}            `json:"cause,omitempty"`
    }
    
    func SimpleConvertToStruct(e error) *ErrorJson {
    	if e != nil {
    		if err := errorx.Cast(e); err != nil {
    			result :=  &ErrorJson{
    				Message:    err.Message(),
    				Properties: err.GetAllPrintableProperties(),
    			}
    			if err.Cause() != nil {
    				result.Cause = SimpleConvertToStruct(err.Cause())
    			}
    			return result
    		} else {
    			return &ErrorJson{
    				Message: e.Error(),
    			}
    		}
    	} else {
    		return nil
    	}
    }
    

    This can be used to create a JSON returned to the end customer as an error message. But the code above is just an example, not a production-ready code! Please, review and test before using it as I haven't at all!

  • Make errors.As work for errorx

    Make errors.As work for errorx

    This PR at this stage is more a design review than a code review. I did not do any code polishing until we agree on whether or not this idea is OK and what to change about it. A concept is simple: if an opinionated errors.As implementation prioritises assignability, let's remove it. A special wrapper is introduced in order to beat this, and *Error::Is() can then do its magic. If a wrapper is then used directly, it works as an error implementation, too. If we go this way, I would also suggest to panic if errorx error is passed as target directly. Such cases are always intentional and by errorx user, so it seems like a good idea not to allow a broken usage at all.

  • delete String function

    delete String function

    Hello. I has a doubt about the String function of namespace.go .Since processing with this function is made by FullName () function, I felt it was not necessary. I deleted the String() function and tested it, but I passed.

  • Use go 1.13 features to improve IsOfType checks

    Use go 1.13 features to improve IsOfType checks

    The only hiccup here is that code for earlier versions must remain unchanged, it doesn't look all that pretty in source code. Also, I could use any ideas for more tests.

  • Add go 1.13 errors feature support

    Add go 1.13 errors feature support

    As tests show, errors.Unwrap and errors.Is work exactly as intended and preserve all existing errorx contracts. On the other hand, errors.As is broken for errorx. It is not a regression of this PR, some tests are added for cases that work, issue will be addressed further in future PRs.

  • Master is not released, but used in redispipe

    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.

  • Copy type on transparent wrapping and hide type modifier transparent

    Copy type on transparent wrapping and hide type modifier transparent

    Currently, transparent types are completely inaccessible, and they don't affect any thing. Therefore there is no need to keep transparent type in an error, and no need to traverse transparent cause chain to reach the type, because it is simpler just to copy type.

    Probably this PR is not complete:

    • should Builder.Transparent() be removed or not? Or should typeModifierTransparent be removed?
    • should tests for transparent namespaces be removed?

    Note: I believe transparent type could be used for injecting new Traits into error (after modification in Error.HasTrait function), and then this PR is not valid.

  • Add helper methods for the

    Add helper methods for the "errors" package

    ~This just calls 'Error.Cause()' but allows the Errors to interop with the 'errors' package from the Go standard library. The main benefit is you can use 'errors.Is' to see if an Error was due to some standard error like context.Canceled.~

    ~Add an Is method to *Error. This is the helper method for errors.Is so you can use that to check your errors. It uses Error.IsOfType if it can (when target is an *Error), otherwise it falls back to iterating through the error chain until it reaches a non-*Error error and calls errors.Is against that error.~

    Add all of the helper methods for the "errors" package behind a build flag so they are only available for Go 1.13+.

  • Improve allocation of properties

    Improve allocation of properties

    benchmark                         old ns/op     new ns/op     delta
    BenchmarkAllocProperty/props0     100           83.0          -17.00%
    BenchmarkAllocProperty/props1     319           172           -46.08%
    BenchmarkAllocProperty/props2     628           292           -53.50%
    BenchmarkAllocProperty/props3     1033          443           -57.12%
    BenchmarkAllocProperty/props4     1485          624           -57.98%
    BenchmarkAllocProperty/props5     2012          828           -58.85%
    BenchmarkAllocProperty/props6     2573          1062          -58.73%
    BenchmarkAllocProperty/props7     3201          1375          -57.04%
    BenchmarkAllocProperty/props8     3829          1591          -58.45%
    
    benchmark                         old allocs     new allocs     delta
    BenchmarkAllocProperty/props0     2              1              -50.00%
    BenchmarkAllocProperty/props1     5              3              -40.00%
    BenchmarkAllocProperty/props2     8              6              -25.00%
    BenchmarkAllocProperty/props3     11             10             -9.09%
    BenchmarkAllocProperty/props4     14             14             +0.00%
    BenchmarkAllocProperty/props5     17             19             +11.76%
    BenchmarkAllocProperty/props6     20             25             +25.00%
    BenchmarkAllocProperty/props7     23             32             +39.13%
    BenchmarkAllocProperty/props8     26             37             +42.31%
    
    benchmark                         old bytes     new bytes     delta
    BenchmarkAllocProperty/props0     176           96            -45.45%
    BenchmarkAllocProperty/props1     672           240           -64.29%
    BenchmarkAllocProperty/props2     1168          432           -63.01%
    BenchmarkAllocProperty/props3     1664          672           -59.62%
    BenchmarkAllocProperty/props4     2160          912           -57.78%
    BenchmarkAllocProperty/props5     2656          1200          -54.82%
    BenchmarkAllocProperty/props6     3152          1536          -51.27%
    BenchmarkAllocProperty/props7     3648          1920          -47.37%
    BenchmarkAllocProperty/props8     4144          2208          -46.72%
    
  • simple travis support

    simple travis support

    Will look like - https://travis-ci.com/isopov/errorx/builds/96400735 after enabling on the repo

    Took https://github.com/stretchr/testify/blob/master/.travis.yml for the base, but did not copy everything - should we check gofmt (see #15) and go vet on travis?

  • Add support for chained panic handlers

    Add support for chained panic handlers

    There is a test to illustrate an issue. If error is not simply propagated along the same call chain by direct returns or panic, it is very useful to see all the places it changed hands. In some real life cases, error may cause panic and be handled as panic payload more than once, and having only the original stack trace is not enough. This PR adds more information here. As a negative note, more clutter in debug output will now be visited upon some simpler cases of panic handling. Panic is not a proper way to handle 'normal' errors in Go, so it is suggested to err on the side of greater verbosity here.

  • Decorate misbehave if err==nil

    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.

  • Errorx doesn't fully respect wrap semantic

    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.

  • Structured stack trace

    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.

  • JSON formatting for an error

    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

  • Improve Documentation

    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?

Simple error handling primitives

errors Package errors provides simple error handling primitives. go get github.com/pkg/errors The traditional error handling idiom in Go is roughly ak

Dec 26, 2022
Declarative error handling for Go.

ErrorFlow Declarative error handling for Go. Motivation Reading list: Don't defer Close() on writable files Error Handling — Problem Overview Proposal

Mar 3, 2022
Generic error handling with panic, recover, and defer.

Generic error handling with panic, recover, and defer.

Aug 25, 2022
brief: a piece of error handling codelet

brief a piece of error handling codelet. this code only demonstrates how to hide sql.ErrNoRows to the caller. the Get() method defined in the pkg/proj

Oct 30, 2021
Just another error handling primitives for golang

errors Just another error handling primitives for golang Install go install github.com/WAY29/errors@latest Usage New error and print error context Th

Feb 19, 2022
🥷 CError (Custom Error Handling)

?? CError (Custom Error Handling) Installation Via go packages: go get github.com/rozturac/cerror Usage Here is a sample CError uses: import ( "gi

Sep 21, 2022
Errors - A lib for handling error gracefully in Go

?? Errors Errors 是一个用于优雅地处理 Go 中错误的库。 Read me in English ??‍ 功能特性 优雅地处理 error,嗯,

Jan 17, 2022
Simple, intuitive and effective error handling for Go

Error Handling with eluv-io/errors-go The package eluv-io/errors-go makes Go error handling simple, intuitive and effective. err := someFunctionThatCa

Jan 19, 2022
Error handling hook & helper function to simplify writing API handler methods in Go.

Error handling hook & helper function to simplify writing API handler methods in Go.

Jan 19, 2022
Try - Idiomatic monadic-ish error handling for go

Try Idiomatic monadic-ish error handling for go. Examples import

Jan 24, 2022
Wraps the normal error and provides an error that is easy to use with net/http.

Go HTTP Error Wraps the normal error and provides an error that is easy to use with net/http. Install go get -u github.com/cateiru/go-http-error Usage

Dec 20, 2021
A flexible error support library for Go

errors Please see http://godoc.org/github.com/spacemonkeygo/errors for info License Copyright (C) 2014 Space Monkey, Inc. Licensed under the Apache Li

Nov 3, 2022
Reduce debugging time while programming Go. Use static and stack-trace analysis to determine which func call causes the error.
Reduce debugging time while programming Go. Use static and stack-trace analysis to determine which func call causes the error.

Errlog: reduce debugging time while programming Introduction Use errlog to improve error logging and speed up debugging while you create amazing code

Nov 18, 2022
A drop-in replacement for Go errors, with some added sugar! Unwrap user-friendly messages, HTTP status code, easy wrapping with multiple error types.
A drop-in replacement for Go errors, with some added sugar! Unwrap user-friendly messages, HTTP status code, easy wrapping with multiple error types.

Errors Errors package is a drop-in replacement of the built-in Go errors package with no external dependencies. It lets you create errors of 11 differ

Dec 6, 2022
A Go (golang) package for representing a list of errors as a single error.

go-multierror go-multierror is a package for Go that provides a mechanism for representing a list of error values as a single error. This allows a fun

Jan 1, 2023
Error tracing and annotation.

errors -- import "github.com/juju/errgo" The errors package provides a way to create and diagnose errors. It is compatible with the usual Go error idi

Nov 3, 2022
A powerful, custom error package for Go

custom-error-go A powerful, custom error package for Go Detailed explanation: https://medium.com/codealchemist/error-handling-in-go-made-more-powerful

Apr 19, 2022
Go extract error codes

go-extract-error-codes Overview This library helps to extract possible error codes from configured go-restful applications quality level: PoC High Lev

Nov 24, 2021
Error interface wrappers for Google's errdetails protobuf types, because they're handy as heck and I want to use them more

Error interface wrappers for Google's errdetails protobuf types, because they're handy as heck and I want to use them more

Nov 18, 2021