|
2 weeks ago | |
---|---|---|
.gitignore | 1 year ago | |
LICENSE | 2 weeks ago | |
README.md | 2 weeks ago | |
danger.go | 2 weeks ago | |
errors.go | 2 weeks ago | |
errors_test.go | 1 year ago | |
go.mod | 1 year ago |
This package provides a greatly simplified error management wrapper for Go that
behaves somewhat similarly to Dave Cheney's errors
package but with far fewer
features. If you need stack management or error formatting, consider using his
package instead.
errors
was created somewhat by accident and before I discovered
pkg/errors
. During the creation of another library, I (re)discovered one of
Go's features that is probably its biggest single pain point for large-ish
applications and one for which there's been much discussion. That is, of course,
error management and the ability--or lack thereof--to include extra data within
an error message without ruining the consumer's ability to parse it. For basic
errors, this isn't an issue. However, for more complex conditions where upstream
consumers may need to know or report additional information to the user or
developer, this quickly causes a descent into chaos.
The semantics of this library are somewhat similar to others. It wraps some of
the basic functionality of Go's errors, namely errors.New
and fmt.Errorf
,
both of which are provided as references in this library. However, it also
defines an ErrorTracer
interface which allows for type switching to be done in
order to determine the type of the error.
General usage of this library starts with errors.NewError
with an argument
containing a string of the error message, e.g.:
var ErrDecoding = errors.NewError("error decoding JSON input")
var ErrEncoding = errors.NewError("error encoding JSON input")
Then, when you encounter a condition where this error is necessary to return,
you can include the upstream error as part of your defined error via .Do()
:
if err := json.Unmarshal(data, map); err != nil {
return ErrDecoding.Do(err)
}
if b, err := json.Marshal(data); err != nil {
return ErrEncoding.Do(err)
}
For comparing errors, two methods are provided to determine whether an error is
of a particular type (.Is()
) or whether it's equal to another error
(.Equal()
):
var err = errors.New("this is an error")
var ErrExample = errors.NewError("this is an example error")
// ...
if e, ok := someError.(errors.ErrorTracer); ok {
if e.Is(ErrExample) {
// True if someError is of the type ErrExample.
}
if e.Equal(ErrExample) {
// True if e is the same as ErrExample. If .Do is called in this
// example, this will not be true. This compares both ErrExample and
// the contents of the original error message.
}
}
To extract and print the original error caught by the code:
if e, ok := err.(errors.ErrorTracer); ok {
fmt.Println(e.OriginalError())
}
There are also three separate ways of ensuring returned errors are wrapped by an
ErrorTracer
:
The first method, .Guarantee()
, accepts an error, and "guarantees" that it
will be of type ErrorTracer
. If it's not, it will be wrapped by the
errors.Error
type. This reduces some boilerplate that would otherwise be
required to perform a cast to ErrorTracer
such that the original triggering
error can be easier to access:
if err != nil {
fmt.Println("original error:", errors.Guarantee(err).OriginalError())
}
The second method, .GuaranteeError()
, accepts two arguments: An error, raised
elsewhere, and an error type returned by errors.NewError
. This attempts to
guarantee one of two things, that either the error is of the type specified, or
that it is wrapped by the specified error. I haven't found a great deal of use
for this specific method, but there are cases where it is warranted, such as
ensuring error conditions returned by a function absolutely match a specific
type for testing elsewhere via .Is()
or .Equal()
. For example:
var ErrDecodeFailed = errors.NewError("decode failed")
func doSomething() error {
if err := decodeSomething(); err != nil {
return errors.GuaranteeError(err, ErrDecodeFailed)
}
}
The third and final installment is relatively new, but there are rare cases
where it's also useful, and that is .GuaranteeOrNil()
. This function either
guarantees that the error is of type ErrorTracer
or is nil
. There are
circumstances where it is useful to either return or embed an ErrorTracer
or
nil
if there is no error, such as when wrapping a response in an outer type
plus its error condition for dispatch over a channel:
type Result struct {
Data map[string]string
Error ErrorTracer
}
c := make(chan ErrorTracer)
// ...
data, err := doSomething()
result := &Result{
Data: data,
Error: errors.GuaranteeOrNil(err),
}
c <- result
errors
also provides a mechanism for embedded metadata within an error type,
but this is a feature I've found to be largely one I have limited use for.
Perhaps one such case might be to store status codes from HTTP clients, e.g.:
var ErrHTTPStatus = errors.NewError("invalid HTTP status")
// ...
response, err := client.Get()
if err != nil {
return ErrHTTPStatus.Do(err).Add("code", response.StatusCode)
}
// ... elsewhere ...
err := fetch("example.com/some/path")
if e := errors.Guarantee(err); e.Is(ErrHTTPStatus) {
meta := e.GetMeta()
if code, ok := meta["code"] {
fmt.Println("status code returned: %v", code)
}
}
Although this section needs to be expanded upon, there are some useful patterns that this library lends itself to.
In particular, creating your own errors
package separate from the rest of your
project and inserting all errors.NewError
statements in one or more files
allows for an easy import of all possible error types, e.g.:
// errors/errors.go
package errors
import "git.destrealm.org/go/errors"
var ErrDecoding = errors.NewError("error decoding values")
var ErrEncoding = errors.NewError("error encoding values")
var ErrReadingResponse = errors.NewError("error reading response")
Then:
// api/client.go
package api
// Notice the "." import.
import (
. "api" // This would likely point to a FQDN with your project/api package.
)
// ...
if data, err := fetch(); err != nil {
return ErrReadingResponse.Do(err)
}
It's also helpful to be aware that errors
exports calls from both the Golang
standard library errors
and fmt
packages. Namely, both erorrs.New
and
fmt.Errorf
are exported as:
e1 := errors.New("...")
e2 := errors.Errorf("...")
This means it is possible to create an ad hoc error type to wrap with .Do()
:
return ErrReadingResponse.Do(errors.Errorf("received status code %d", response.StatusCode))
Be aware that unlike pkg/errors
, this library does not include any method for
examining the error stack trace or even the stack of errors. In theory, you can
do this yourself by recursively comparing the contents of .OriginalError()
, if
it implements the ErrorTracer
interface then continue, and print out the tree
until you hit the base error type (that is, something that only implements
.Error()
).
This library is intentionally very spartan and is unlikely to grow in extra functionality. We may eventually implement a method of collecting information from the error stack, in addition to a handful of other useful features. We may also implement a deep-equals to determine if the entire error plus its stack is identical, although this may be unlikely.
Part of the rationale for not including an ability to recursively step through
the error stack is that most use cases we've encountered with this library tend
toward wrapping Go errors one level deep. Rarely have we required nesting
ErrorTracer
types, and in most cases, calling .OriginalError()
will return
the first Go error returned. If you follow this convention, you'll likely find
limited need for recursively collating errors wrapped by this library. However,
it may be useful to include a .WrapIfNotWrapped()
package-level function (or
similar) that would only wrap an error within the specified type if a) it isn't
already wrapped and b) isn't nil.
Pull requests are welcome. Please be aware that it is the author's intent that this library remain simple and easy to use. As this documentation is also rather bare, if you would like to add anything to make it clearer without sacrificing its conciseness, such requests are also welcome.
errors
is offered under an NCSA license, which is essentially a 3-clause BSD
license combined with the MIT license for clarity. This is the same license used
by the LLVM and Clang projects and arguably resolves issues that affect both the
BSD and MIT licenses individually. In particular, this license grants clear
distribution rights for both the software and its associated documentation.