Home

Awesome

cockroachdb/errors: Go errors with network portability

This library aims to be used as a drop-in replacement to github.com/pkg/errors and Go's standard errors package. It also provides network portability of error objects, in ways suitable for distributed systems with mixed-version software compatibility.

It also provides native and comprehensive support for PII-free details and an opt-in Sentry.io reporting mechanism that automatically formats error details and strips them of PII.

See also the design RFC.

Build Status Go Reference

Table of contents:

Features

FeatureGo's <1.13 errorsgithub.com/pkg/errorsGo 1.13 errors/xerrorscockroachdb/errors
error constructors (New, Errorf etc)
error causes (Cause / Unwrap)
cause barriers (Opaque / Handled)
errors.As(), errors.Is()
automatic error wrap when format ends with : %w
standard wrappers with efficient stack trace capture
transparent protobuf encode/decode with forward compatibility
errors.Is() recognizes errors across the network
comprehensive support for PII-free reportable strings
support for both Cause() and Unwrap() go#31778
standard error reports to Sentry.io
wrappers to denote assertion failures
wrappers with issue tracker references
wrappers for user-facing hints and details
wrappers to attach secondary causes
wrappers to attach logtags details from context.Context
errors.FormatError(), Formatter, Printer(under construction)
errors.SafeFormatError(), SafeFormatter
wrapper-aware IsPermission(), IsTimeout(), IsExist(), IsNotExist()

"Forward compatibility" above refers to the ability of this library to recognize and properly handle network communication of error types it does not know about, for example when a more recent version of a software package sends a new error object to another system running an older version of the package.

How to use

What comes out of an error?

Error detailError() and format %s/%q/%vformat %+vGetSafeDetails()Sentry report via ReportError()
main message, eg New()visiblevisibleyes (CHANGED IN v1.6)full (CHANGED IN v1.6)
wrap prefix, eg WithMessage()visible (as prefix)visibleyes (CHANGED IN v1.6)full (CHANGED IN v1.6)
stack trace, eg WithStack()not visiblesimplifiedyesfull
hint , eg WithHint()not visiblevisiblenotype only
detail, eg WithDetail()not visiblevisiblenotype only
assertion failure annotation, eg WithAssertionFailure()not visiblevisiblenotype only
issue links, eg WithIssueLink(), UnimplementedError()not visiblevisibleyesfull
safe details, eg WithSafeDetails()not visiblenot visibleyesfull
telemetry keys, eg. WithTelemetryKey()not visiblevisibleyesfull
secondary errors, eg. WithSecondaryError(), CombineErrors()not visiblevisibleredacted, recursivelyredacted, recursively
barrier origins, eg. Handled()not visiblevisibleredacted, recursivelyredacted, recursively
error domain, eg. WithDomain()not visiblevisibleyesfull
context tags, eg. WithContextTags()not visiblevisiblekeys visible, values redactedkeys visible, values redacted

Available error leaves

An error leaf is an object that implements the error interface, but does not refer to another error via a Unwrap() or Cause() method.

Available wrapper constructors

An error wrapper is an object that implements the error interface, and also refers to another error via an Unwrap() (preferred) and/or Cause() method.

All wrapper constructors can be applied safely to a nil error: they behave as no-ops in this case:

// The following:
// if err := foo(); err != nil {
//    return errors.Wrap(err, "foo")
// }
// return nil
//
// is not needed. Instead, you can use this:
return errors.Wrap(foo(), "foo")

Providing PII-free details

The library support PII-free strings essentially as follows:

The following strings from this library are considered to be PII-free, and thus included in Sentry reports automatically:

It is possible to opt additional in to Sentry reporting, using either of the following methods:

For more details on how Sentry reports are built, see the report sub-package.

Building your own error types

You can create an error type as usual in Go: implement the error interface, and, if your type is also a wrapper, the errors.Wrapper interface (an Unwrap() method). You may also want to implement the Cause() method for backward compatibility with github.com/pkg/errors, if your project also uses that.

If your error type is a wrapper, you should implement a Format() method that redirects to errors.FormatError(), otherwise %+v will not work. Additionally, if your type has a payload not otherwise visible via Error(), you may want to implement errors.SafeFormatter. See making %+v work with your type below for details.

Finally, you may want your new error type to be portable across the network.

If your error type is a leaf, and already implements proto.Message (from gogoproto), you are all set and the errors library will use that automatically. If you do not or cannot implement proto.Message, or your error type is a wrapper, read on.

At a minimum, you will need a decoder function: while cockroachdb/errors already does a bunch of encoding/decoding work on new types automatically, the one thing it really cannot do on its own is instantiate a Go object using your new type.

Here is the simplest decode function for a new leaf error type and a new wrapper type:

// note: we use the gogoproto `proto` sub-package.
func yourDecode(_ string, _ []string, _ proto.Message) error {
   return &yourType{}
}

func init() {
   errors.RegisterLeafEncoder((*yourType)(nil), yourDecodeFunc)
}

func yourDecodeWrapper(cause error, _ string, _ []string, _ proto.Message) error {
   // Note: the library already takes care of encoding/decoding the cause.
   return &yourWrapperType{cause: cause}
}

func init() {
   errors.RegisterWrapperDecoder((*yourWrapperType)(nil), yourDecodeWrapper)
}

In the case where your type does not have any other field (empty struct for leafs, just a cause for wrappers), this is all you have to do.

(See the type withAssertionFailure in assert/assert.go for an example of this simple case.)

If your type does have additional fields, you may still not need a custom encoder. This is because the library automatically encodes/decodes the main error message and any safe strings that your error types makes available via the errors.SafeDetailer interface (the SafeDetails() method).

Say, for example, you have the following leaf type:

type myLeaf struct {
   code int
}

func (m *myLeaf) Error() string { return fmt.Sprintf("my error: %d" + m.code }

In that case, the library will automatically encode the result of calling Error(). This string will then be passed back to your decoder function as the first argument. This makes it possible to decode the code field exactly:

func myLeafDecoder(msg string, _ []string, _ proto.Message) error {
	codeS := strings.TrimPrefix(msg, "my error: ")
	code, _ := strconv.Atoi(codeS)
	// Note: error handling for strconv is omitted here to simplify
	// the explanation. If your decoder function should fail, simply
	// return a `nil` error object (not another unrelated error!).
	return &myLeaf{code: code}
}

Likewise, if your fields are PII-free, they are safe to expose via the errors.SafeDetailer interface. Those strings also get encoded automatically, and get passed to the decoder function as the second argument.

For example, say you have the following leaf type:

type myLeaf struct {
   // both fields are PII-free.
   code int
   tag string
}

func (m *myLeaf) Error() string { ... }

Then you can expose the fields as safe details as follows:

func (m *myLeaf) SafeDetails() []string {
  return []string{fmt.Sprintf("%d", m.code), m.tag}
}

(If the data is PII-free, then it is good to do this in any case: it enables any network system that receives an error of your type, but does not know about it, to still produce useful Sentry reports.)

Once you have this, the decode function receives the strings and you can use them to re-construct the error:

func myLeafDecoder(_ string, details []string, _ proto.Message) error {
    // Note: you may want to test the length of the details slice
	// is correct.
    code, _ := strconv.Atoi(details[0])
    tag := details[1]
	return &myLeaf{code: code, tag: tag}
}

(For an example, see the withTelemetry type in telemetry/with_telemetry.go.)

The only case where you need a custom encoder is when your error type contains some fields that are not reflected in the error message (so you can't extract them back from there), and are not PII-free and thus cannot be reported as "safe details".

To take inspiration from examples, see the following types in the library that need a custom encoder:

Making %+v work with your type

In short:

The example withHTTPCode wrapper included in the source tree achieves this as follows:

// Format() implements fmt.Formatter, is required until Go knows about FormatError.
func (w *withHTTPCode) Format(s fmt.State, verb rune) { errors.FormatError(w, s, verb) }

// FormatError() formats the error.
func (w *withHTTPCode) SafeFormatError(p errors.Printer) (next error) {
	// Note: no need to print out the cause here!
	// FormatError() knows how to do this automatically.
	if p.Detail() {
		p.Printf("http code: %d", errors.Safe(w.code))
	}
	return w.cause
}

Technical details follow:

Ensuring errors.Is works when errors/packages are renamed

If a Go package containing a custom error type is renamed, or the error type itself is renamed, and errors of this type are transported over the network, then another system with a different code layout (e.g. running a different version of the software) may not be able to recognize the error any more via errors.Is.

To ensure that network portability continues to work across multiple software versions, in the case error types get renamed or Go packages get moved / renamed / etc, the server code must call errors.RegisterTypeMigration() from e.g. an init() function.

Example use:

 previousPath := "github.com/old/path/to/error/package"
 previousTypeName := "oldpackage.oldErrorName"
 newErrorInstance := &newTypeName{...}
 errors.RegisterTypeMigration(previousPath, previousTypeName, newErrorInstance)

Error composition (summary)

ConstructorComposes
NewNewWithDepth (see below)
Errorf= Newf
NewfNewWithDepthf (see below)
WithMessagecustom wrapper with message prefix and knowledge of safe strings
WrapWrapWithDepth (see below)
WrapfWrapWithDepthf (see below)
AssertionFailedAssertionFailedWithDepthf (see below)
NewWithDepthcustom leaf with knowledge of safe strings + WithStackDepth (see below)
NewWithDepthfcustom leaf with knowledge of safe strings + WithSafeDetails + WithStackDepth
WithMessagefcustom wrapper with message prefix and knowledge of safe strings
WrapWithDepthWithMessage + WithStackDepth
WrapWithDepthfWithMessagef + WithStackDepth
AssertionFailedWithDepthfNewWithDepthf + WithAssertionFailure
NewAssertionErrorWithWrappedErrfHandledWithMessagef (barrier) + WrapWithDepthf + WithAssertionFailure
JoinJoinWithDepth (see below)
JoinWithDepthmulti-cause wrapper + WithStackDepth

API (not constructing error objects)

The following is a summary of the non-constructor API functions, grouped by category. Detailed documentation can be found at: https://pkg.go.dev/github.com/cockroachdb/errors

// Access causes.
func UnwrapAll(err error) error
func UnwrapOnce(err error) error
func Cause(err error) error // compatibility
func Unwrap(err error) error // compatibility
type Wrapper interface { ... } // compatibility

// Error formatting.
type Formatter interface { ... } // compatibility, not recommended
type SafeFormatter interface { ... }
type Printer interface { ... }
func FormatError(err error, s fmt.State, verb rune)
func Formattable(err error) fmt.Formatter

// Identify errors.
func Is(err, reference error) bool
func IsAny(err error, references ...error) bool
func If(err error, pred func(err error) (interface{}, bool)) (interface{}, bool)
func As(err error, target interface{}) bool

// Encode/decode errors.
type EncodedError // this is protobuf-encodable
func EncodeError(ctx context.Context, err error) EncodedError
func DecodeError(ctx context.Context, enc EncodedError) error

// Register encode/decode functions for custom/new error types.
func RegisterLeafDecoder(typeName TypeKey, decoder LeafDecoder)
func RegisterLeafEncoder(typeName TypeKey, encoder LeafEncoder)
func RegisterWrapperDecoder(typeName TypeKey, decoder WrapperDecoder)
func RegisterWrapperEncoder(typeName TypeKey, encoder WrapperEncoder)
func RegisterWrapperEncoderWithMessageOverride (typeName TypeKey, encoder WrapperEncoderWithMessageOverride)
func RegisterMultiCauseEncoder(theType TypeKey, encoder MultiCauseEncoder)
func RegisterMultiCauseDecoder(theType TypeKey, decoder MultiCauseDecoder)
type LeafEncoder = func(ctx context.Context, err error) (msg string, safeDetails []string, payload proto.Message)
type LeafDecoder = func(ctx context.Context, msg string, safeDetails []string, payload proto.Message) error
type WrapperEncoder = func(ctx context.Context, err error) (msgPrefix string, safeDetails []string, payload proto.Message)
type WrapperEncoderWithMessageOverride = func(ctx context.Context, err error) (msgPrefix string, safeDetails []string, payload proto.Message, overrideError bool)
type WrapperDecoder = func(ctx context.Context, cause error, msgPrefix string, safeDetails []string, payload proto.Message) error
type MultiCauseEncoder = func(ctx context.Context, err error) (msg string, safeDetails []string, payload proto.Message)
type MultiCauseDecoder = func(ctx context.Context, causes []error, msgPrefix string, safeDetails []string, payload proto.Message) error

// Registering package renames for custom error types.
func RegisterTypeMigration(previousPkgPath, previousTypeName string, newType error)

// Sentry reports.
func BuildSentryReport(err error) (*sentry.Event, map[string]interface{})
func ReportError(err error) (string)

// Stack trace captures.
func GetOneLineSource(err error) (file string, line int, fn string, ok bool)
type ReportableStackTrace = sentry.StackTrace
func GetReportableStackTrace(err error) *ReportableStackTrace

// Safe (PII-free) details.
type SafeDetailPayload struct { ... }
func GetAllSafeDetails(err error) []SafeDetailPayload
func GetSafeDetails(err error) (payload SafeDetailPayload)

// Obsolete APIs.
type SafeMessager interface { ... }
func Redact(r interface{}) string

// Aliases redact.Safe.
func Safe(v interface{}) SafeMessager

// Assertion failures.
func HasAssertionFailure(err error) bool
func IsAssertionFailure(err error) bool

// User-facing details and hints.
func GetAllDetails(err error) []string
func FlattenDetails(err error) string
func GetAllHints(err error) []string
func FlattenHints(err error) string

// Issue links / URL wrappers.
func HasIssueLink(err error) bool
func IsIssueLink(err error) bool
func GetAllIssueLinks(err error) (issues []IssueLink)

// Unimplemented errors.
func HasUnimplementedError(err error) bool
func IsUnimplementedError(err error) bool

// Telemetry keys.
func GetTelemetryKeys(err error) []string

// Domain errors.
type Domain
const NoDomain Domain
func GetDomain(err error) Domain
func NamedDomain(domainName string) Domain
func PackageDomain() Domain
func PackageDomainAtDepth(depth int) Domain
func EnsureNotInDomain(err error, constructor DomainOverrideFn, forbiddenDomains ...Domain) error
func NotInDomain(err error, doms ...Domain) bool

// Context tags.
func GetContextTags(err error) []*logtags.Buffer