package
0.0.0-20250305165956-269064b29c6f
Repository: https://github.com/bryk-io/pkg.git
Documentation: pkg.go.dev

# README

Package errors

When dealing with unexpected or undesired behavior on any system (like issues and exceptions) the more information available, structured and otherwise, the better. Preserving error structure and context is particularly important since, in general, string comparisons on error messages are vulnerable to injection and can even cause security problems. On distributed systems is very useful, and often required, to preserve these details across service boundaries as well.

The main goals of this package are:

  • Provide a simple, extensible and "familiar" implementation that can be easily used as a drop-in replacement for the standard "errors" package and popular 3rd party libraries.
  • Preserve the entire structure of errors across the wire using pluggable codecs.
  • Produce portable and PII-safe error reports. These reports can then be sent to any 3rd party service or webhook.
  • Enable fast, reliable and secure determination of whether a particular cause is present (not relying on the presence of a substring in the error message).
  • Being easily composable; by making it extensible with additional error annotations and supporting special behavior on custom error types.

Inspiration

This library is mainly inspired on the original https://github.com/cockroachdb/errors package, while adding some specific adjustments. For additional information about the original package refer to PR-36987.

Motivation

Go provides 4 "idiomatic" ways to inspect errors:

  1. Reference comparison to global objects. err == io.EOF

  2. Type assertions to known error types. err.(*os.PathError)

  3. Predicate provided by library. os.IsNotExists(err)

  4. String comparison on the result of err.Error()

Method 1 breaks down when using wrapped errors, or when transferring errors over the network.

Method 2 breaks down if the error object is converted to a different type. When wire representations are available, the method is generally reliable; however, if errors are implemented as a chain of causes, care should be taken to perform the test on all the intermediate levels.

Method 3 is generally reliable although the predicates in the standard library obviously do not know about any additional custom types. Also, the implementation of the predicate method can be cumbersome if one must test errors from multiple packages (dependency cycles). This method loses its reliability if the predicate itself relies on one of the other methods in a way that's unreliable.

Method 4 is the most problematic and unreliable.

Usage

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

  • To create a new error instance use constructor methods New() or Errorf(). The stack trace of the error will point to the line the method is called.
  • You can use Opaque to capture an error cause but make it invisible to Unwrap() or Is(). This is particularly useful when a new error occurs while handling another one, and the original error must be "hidden".

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

  • Wrapper constructors, i.e., Wrap() can be applied safely to a nil error; the function will behave as no-op in this case.

Custom error types

You can personalize the behavior of your custom error types by providing implementations for the following methods:

  • Cause() error
  • Unwrap() error
  • Is(target error) bool

Redactable Details

You can easily generate a redactable message container that supports manually hiding and disclosing any additional parameters. This is particularly useful to avoid accidentally dumping sensitive details to logs or error messages.

// Create a redactable message. All arguments are considered sensitive.
secret := SensitiveMessage("my name is %s (or %s)", "bond", "007")

// Printing the message will remove any arguments used to generate the
// message.
fmt.Println(secret)

// You can use the `%+v` formatting verb to manually disclose the provided
// arguments.
fmt.Printf("%+v", secret)

// When using a redactable message to create an error instance, secret details
// will never be printed out; not even when using the `%+v` formatting verb to
// generate a portable stacktrace.
err := New(secret)
fmt.Printf("%+v", err)

Example

Consider the following dummy code consisting of several levels of function calling; each one wrapping potential errors bubbling up from lower levels.

// Nested chain of function calls.
// sampleA -> wraps
//  sampleB -> wraps
//   sampleC -> wraps
//    sampleD -> wraps
//     sampleE = returns the original error
func sampleA() error { return Wrap(sampleB(), "a") }
func sampleB() error { return Wrap(sampleC(), "b") }
func sampleC() error { return Wrap(sampleD(), "c") }
func sampleD() error { return Wrap(sampleE(), "d") }
func sampleE() error { return New("deep error") }

Your function call this call, receives and error and you need to inspect it and use it productively.

func myAwesomeFunction() {
  err := sampleA()

  // The most basic thing you can do with the error is log/print it.
  fmt.Println(err.Error())
    // This will print:
    // a: b: c: d: deep error

  // You can also use `Unwrap` to go one level down in the error
  // "chain". For example:
  fmt.Println(Unwrap(err).Error())
    // This will print:
    // b: c: d: deep error

  // Or go straight to the root cause, i.e., the deep-most error
  // in the chain.
  fmt.Println(Cause(err).Error())
    // This will print:
    // deep error
}

Stack Traces

Of course there are more interesting details you can get from your errors. When diagnosing issues, the more details at your disposal the better. An important tool at your disposal are stack traces.

func myAwesomeFunction() {
  err := sampleA()

  // Using the '%v' format command with your error value will
  // output a trace formatted as in the standard library
  // `runtime/debug.Stack()`
  fmt.Printf("%v", err)

The trace produced will be something similar to:

a: b: c: d: deep error
/home/ben/go/src/bryk-io/pkg/errors/api_test.go:199 (0x1024a8e4b)
  sampleE: func sampleE() error { return New("deep error") }
/home/ben/go/src/bryk-io/pkg/errors/api_test.go:198 (0x1024a8e38)
  sampleD: func sampleD() error { return Wrap(sampleE(), "d") }
/home/ben/go/src/bryk-io/pkg/errors/api_test.go:197 (0x1024a8deb)
  sampleC: func sampleC() error { return Wrap(sampleD(), "c") }
/home/ben/go/src/bryk-io/pkg/errors/api_test.go:196 (0x1024a8d9b)
  sampleB: func sampleB() error { return Wrap(sampleC(), "b") }
/home/ben/go/src/bryk-io/pkg/errors/api_test.go:195 (0x1024a8d4b)
  sampleA: func sampleA() error { return Wrap(sampleB(), "a") }
/home/ben/go/src/bryk-io/pkg/errors/api_test.go:14 (0x1024a6cdb)
  TestSample: err := sampleA()
/opt/homebrew/Cellar/go/1.19.5/libexec/src/testing/testing.go:1446 (0x1023ebe1b)
  tRunner: fn(t)

The standard trace, while helpful, is filled with local details. For example all the paths from my local filesystem. For this reason, this package also supports the %+v format command that will produce a more portable and friendlier stack trace output.

func myAwesomeFunction() {
  err := sampleA()

  // Using the '%v' format command with your error value will
  // output a more portable trace.
  fmt.Printf("%+v", err)

This time, the trace produced will be formatted like the following:

a: b: c: d: deep error
‹0› GOPATH/src/bryk-io/pkg/errors/api_test.go:199 (0x102190e4b)
  sampleE: func sampleE() error { return New("deep error") }
‹1› GOPATH/src/bryk-io/pkg/errors/api_test.go:198 (0x102190e38)
  sampleD: func sampleD() error { return Wrap(sampleE(), "d") }
‹2› GOPATH/src/bryk-io/pkg/errors/api_test.go:197 (0x102190deb)
  sampleC: func sampleC() error { return Wrap(sampleD(), "c") }
‹3› GOPATH/src/bryk-io/pkg/errors/api_test.go:196 (0x102190d9b)
  sampleB: func sampleB() error { return Wrap(sampleC(), "b") }
‹4› GOPATH/src/bryk-io/pkg/errors/api_test.go:195 (0x102190d4b)
  sampleA: func sampleA() error { return Wrap(sampleB(), "a") }
‹5› GOPATH/src/bryk-io/pkg/errors/api_test.go:14 (0x10218ecdb)
  TestSample: err := sampleA()
‹6› GOROOT/src/testing/testing.go:1446 (0x1020d3e1b)
  tRunner: fn(t)

Additional Information

Stack traces are a great place to start diagnosing issues, but of course, the more information available to you the better. This package allow you to annotate your error with additional contextual information such as: hints, events and tags.

func myAwesomeFunction() {
  err := sampleA()
  
  // First, cast the error as an "Error" instance provided
  // by this package.
  var te *Error
  if As(err, &te) {
    // Hints provide free-form contextual details.
    te.AddHint("hints can provide additional context about an issue")
    te.AddHint("this was just a test")

    // Tags are usually used for: grouping, filtering and statistic
    // analysis in general.
    te.SetTag("env", "testing")
    te.SetTag("user", "rick")
    te.SetTag("paying_customer", true)

    // You can also add relevant events produced along
    // the error chain. This is often useful when diagnosing
    // complex issues involving many components/services.
    te.AddEvent(Event{
      Kind:    "console",
      Message: "additional debugging information",
    })
  }

  // Then, use the "extended" format command `%+v`. This time
  // the output will include not only the portable stack trace
  // but also all the additional contextual information available
  // in the error.
  fmt.Printf("%+v", te)

The output produced from this error will contain a lot more details that, hopefully, will help diagnose and fix the issue.

a: b: c: d: deep error
‹0› GOPATH/src/bryk-io/pkg/errors/api_test.go:202 (0x1008a909b)
 sampleE: func sampleE() error { return New("deep error") }
‹1› GOPATH/src/bryk-io/pkg/errors/api_test.go:201 (0x1008a9088)
 sampleD: func sampleD() error { return Wrap(sampleE(), "d") }
‹2› GOPATH/src/bryk-io/pkg/errors/api_test.go:200 (0x1008a903b)
 sampleC: func sampleC() error { return Wrap(sampleD(), "c") }
‹3› GOPATH/src/bryk-io/pkg/errors/api_test.go:199 (0x1008a8feb)
 sampleB: func sampleB() error { return Wrap(sampleC(), "b") }
‹4› GOPATH/src/bryk-io/pkg/errors/api_test.go:198 (0x1008a8f9b)
 sampleA: func sampleA() error { return Wrap(sampleB(), "a") }
‹5› GOPATH/src/bryk-io/pkg/errors/api_test.go:14 (0x1008a6deb)
 TestSample: err := sampleA()
‹6› GOROOT/src/testing/testing.go:1446 (0x1007ebe1b)
 tRunner: fn(t)
‹hints›
 - hints can provide additional context about an issue
 - this was just a test
‹tags›
 - env=testing
 - user=rick
 - paying_customer=true
‹events›
 - (console) additional debugging information

Reports

Finally, having all that information displayed in the console is already helpful, but being able to collect it elsewhere will be even more so. That's what Report allows us to do. Report takes in and error instance and a Codec and is responsible of producing a portable representation of the error's contents. As a reference, this package includes a CodecJSON implementation.

For example, you can produce a JSON report of the above error and submitted via HTTP to your monitoring system.

func myAwesomeFunction() {
  err := sampleA()
  
  // First, cast the error as an "Error" instance provided
  // by this package.
  var te *Error
  if As(err, &te) {
    // ... add same additional details as before ...
    // omitted for brevity
  }

  // Produce JSON error report
  js, _ := Report(err, CodecJSON(true))
  fmt.Printf("%s", js)

The produced report will be:

{
  "error": "a: b: c: d: deep error",
  "events": [
    {
      "kind": "console",
      "message": "additional debugging information",
      "stamp": 1674845182688
    }
  ],
  "hints": [
    "hints can provide additional context about an issue",
    "this was just a test"
  ],
  "stamp": 1674845182688,
  "tags": {
    "env": "testing",
    "paying_customer": true,
    "user": "rick"
  },
  "trace": [
    {
      "file": "GOPATH/src/bryk-io/pkg/errors/api_test.go",
      "line_number": 198,
      "function": "sampleE",
      "package": "go.bryk.io/pkg/errors",
      "source_line": "func sampleE() error { return New(\"deep error\") }",
      "program_counter": 4297461947
    },
    {
      "file": "GOPATH/src/bryk-io/pkg/errors/api_test.go",
      "line_number": 197,
      "function": "sampleD",
      "package": "go.bryk.io/pkg/errors",
      "source_line": "func sampleD() error { return Wrap(sampleE(), \"d\") }",
      "program_counter": 4297461928
    },
    {
      "file": "GOPATH/src/bryk-io/pkg/errors/api_test.go",
      "line_number": 196,
      "function": "sampleC",
      "package": "go.bryk.io/pkg/errors",
      "source_line": "func sampleC() error { return Wrap(sampleD(), \"c\") }",
      "program_counter": 4297461851
    },
    {
      "file": "GOPATH/src/bryk-io/pkg/errors/api_test.go",
      "line_number": 195,
      "function": "sampleB",
      "package": "go.bryk.io/pkg/errors",
      "source_line": "func sampleB() error { return Wrap(sampleC(), \"b\") }",
      "program_counter": 4297461771
    },
    {
      "file": "GOPATH/src/bryk-io/pkg/errors/api_test.go",
      "line_number": 194,
      "function": "sampleA",
      "package": "go.bryk.io/pkg/errors",
      "source_line": "func sampleA() error { return Wrap(sampleB(), \"a\") }",
      "program_counter": 4297461691
    },
    {
      "file": "GOPATH/src/bryk-io/pkg/errors/api_test.go",
      "line_number": 14,
      "function": "TestSample",
      "package": "go.bryk.io/pkg/errors",
      "source_line": "err := sampleA()",
      "program_counter": 4297453035
    },
    {
      "file": "GOROOT/src/testing/testing.go",
      "line_number": 1446,
      "function": "tRunner",
      "package": "testing",
      "source_line": "fn(t)",
      "program_counter": 4296687131
    }
  ]
}

# Functions

As unwraps `err` sequentially looking for an error that can be assigned to `target`, which must be a pointer.
Cause will recursively retrieve the topmost error which does not provide a cause, which is assumed to be the original failure condition.
CodecJSON encodes error data as JSON documents.
Combine the error given as first argument with an annotation that carries the error given as second argument.
Errorf returns a new root error (i.e., without a cause) instance which stacktrace will point to the line of code that called this function.
FromRecover is a utility function to facilitate obtaining a useful error instance from a panicked goroutine.
Is detects whether the error is equal to a given error.
IsAny detects whether the error is equal to any of the provided target errors.
New returns a new root error (i.e., without a cause) instance from the given value.
Opaque returns an error with the same formatting as `err` but that does not match `err` and cannot be unwrapped.
ParsePanic allows you to get an error object from the output of a go program that panicked.
Report an error instance by generating a portable/transmissible representation of it using the provided codec.
SensitiveMessage returns a redactable message container.
Unwrap unpacks wrapped errors.
WithStack returns a new root error (i.e., without a cause) instance which stacktrace will point to the line of code that called this function.
WithStackAt returns a new root error (i.e., without a cause) instance which stacktrace will point to the line of code that called this function; minus number of stacks specified in `at`.
Wrap a given error into another one, this allows to create or expand an error cause chain.
Wrapf returns a wrapped version of the provided error using a formatted string as prefix.

# Structs

Error is an error with an attached stacktrace.
Event instances can be used to provided additional contextual information for an error.
A StackFrame contains all necessary information about a specific line in a callstack.

# Interfaces

Codec implementations provide a pluggable way to manage error across service boundaries; for example when transmitting error messages through a network.
HasStack is implemented by error types that natively provide robust stack traces.
Redactable represents a message that contains some form of private or sensitive information that should be redacted when used as an error or log message.