errors

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 2, 2025 License: MIT Imports: 13 Imported by: 16

README

errors - API for creating exceptions and alerting

GoDoc unit tests report card codecov

Install:

go get github.com/memsql/errors

Package errors provides an API for creating exceptions and alerting.

This is intended to be a replacement for Go's standard library errors package. You can import "github.com/memsql/errors" instead of "errors".

Verbose Messages

Use New or Errorf to produce an error that can be formatted with verbose details, including a stack trace. To see the verbose details, format using %+v instead of %v, %s or calling err.Error().

Alert and Capture

Applications can call RegisterCapture to persist errors to logs, a backend database, or a third-party service. When Alert or Alertf is called, the error will be passed to each registered handler, which is responsible for peristing. The capture handler returns an identifier which becomes part of the error message, as a suffix enclosed in square brackets.

Expand and Expunge

The Expand helper adds information to an error, while Expunge is intended to remove information that end users don't need to see. Both are intended to be deferred, using a pattern like this,

func FindThing(id string) (err error) {
  // include the id, and verbose stack, with all errors
  defer errors.Expand(&err, "thing (%q) not found", id)

  // ...
}

When returning errors which wrap other errors, Expunge can be used to hide underlying details, to make a more user-friendly message. It assumes that error message text follows specific conventions, described below.

Message Conventions

Error messages include static parts and dynamic parts. If the error is “/tmp/foo.txt not found”, then “/tmp/foo.txt” is the dynamic part and “not found” is the static part. The dynamic parts present detail to the reader. The static part is useful as well. For example, to determine the line of code where it originated, or how frequently the error is triggered. This package follows a convention to easily work with both static and dynamic parts of error messages:

  • Dynamic text SHOULD appear in parenthesis.

  • Static text SHOULD be grammatically correct, with or without the dynamic parts.

Following these guidelines, the backend might produce an error: “file ("/tmp/foo.txt”) not found”.

The static text should make sense even when the dynamic parts are removed. Imagine running a whole log file through a regular expression that removes the parentheticals. This stripped log file should still make sense to the reader. In our example, the stripped text would be “file not found”, which makes sense (while just “not found” would not make sense).

The colon character “:” has special significance in error message. It implies that an error message has been “wrapped” with another message, providing additional context.

// avoid this!
return errors.Errorf("invalid widget id: %s", id)

// do this
return errors.Errorf("failed to parse widget id (%q): %w", id, err)
  • Error messages SHOULD use the colon “:” only when wrapping another error.

Documentation

Overview

Package errors provides an API for creating exceptions and alerting.

This is intended to be a replacement for Go's standard library errors package. You can import "github.com/memsql/errors" instead of "errors".

Verbose Messages

Use New or Errorf to produce an error that can be formatted with verbose details, including a stack trace. To see the verbose details, format using `%+v` instead of `%v`, `%s` or calling err.Error().

Alert and Capture

Applications can call RegisterCapture to persist errors to logs, a backend database, or a third-party service. When Alert or Alertf is called, the error will be passed to each registered handler, which is responsible for peristing. The capture handler returns an identifier which becomes part of the error message, as a suffix enclosed in square brackets.

Expand and Expunge

The Expand helper adds information to an error, while Expunge is intended to remove information that end users don't need to see. Both are intended to be deferred, using a pattern like this,

func FindThing(id string) (err error) {
  // include the id, and verbose stack, with all errors
  defer errors.Expand(&err, "thing (%q) not found", id)

  // ...
}

When returning errors which wrap other errors, Expunge can be used to hide underlying details, to make a more user-friendly message. It assumes that error message text follows specific conventions, described below.

Message Conventions

Error messages include static parts and dynamic parts. If the error is “/tmp/foo.txt not found”, then “/tmp/foo.txt” is the dynamic part and “not found” is the static part. The dynamic parts present detail to the reader. The static part is useful as well. For example, to determine the line of code where it originated, or how frequently the error is triggered. This package follows a convention to easily work with both static and dynamic parts of error messages:

* Dynamic text SHOULD appear in parenthesis.

* Static text SHOULD be grammatically correct, with or without the dynamic parts.

Following these guidelines, the backend might produce an error: “file ("/tmp/foo.txt”) not found”.

The static text should make sense even when the dynamic parts are removed. Imagine running a whole log file through a regular expression that removes the parentheticals. This stripped log file should still make sense to the reader. In our example, the stripped text would be “file not found”, which makes sense (while just “not found” would not make sense).

The colon character “:” has special significance in error message. It implies that an error message has been “wrapped” with another message, providing additional context.

// avoid this!
return errors.Errorf("invalid widget id: %s", id)

// do this
return errors.Errorf("failed to parse widget id (%q): %w", id, err)

* Error messages SHOULD use the colon “:” only when wrapping another error.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	As     = errors.As
	Is     = errors.Is
	Join   = errors.Join
	Unwrap = errors.Unwrap
)
View Source
var CaptureTimeout = 500 * time.Millisecond

CaptureTimeout limits how long to wait for a capture ID to be returned from a capture handler.

Functions

func Alert

func Alert(err error) error

Alert sends an error to all registered capture handlers. Capture handlers produce verbose logs and alerts. This should be called only for errors that require human attention to address (our developers or SREs). It should not be called for run-of-the-mill errors that are handled in code or returned to portal users.

func Alertf

func Alertf(format string, a ...interface{}) error

Alertf produces an error and alerts. It is equivalent to calling Errorf() and then Alert().

func Annotate added in v0.2.0

func Annotate(err error, values ...any) error

func Annotation added in v0.2.0

func Annotation[T any](err error) (T, bool)

Annotation looks at the arguments supplied to Errrof(), Wrapf(), and Annotate(), looking to see if any of them match the type T. If so, it returns the value and true. Otherwise it returns an empty T and false.

func Expand

func Expand(exception *error, format string, a ...interface{})

Expand rewites an error message, when an error is non-nil.

This is intended to be invoked as a deferred function, as a convenient way to add details to an error immediately before returning it.

Example
package main

import (
	"fmt"

	"github.com/memsql/errors"
)

func main() {
	// mustBeEven is at odds with odds.
	mustBeEven := func(input int) error {
		if (input % 2) == 1 {
			return errors.New("not even")
		}
		return nil
	}

	// processEvenNumber uses errors.Expand() to add details when returning non-nil error.
	processEvenNumber := func(input int) (err error) {
		//
		// Expand() adds details to non-nil err, leaves nil error as-is.
		//
		defer errors.Expand(&err, "failed to process number (%d)", input)

		return mustBeEven(input)
	}

	err := processEvenNumber(1) // fails because not even
	fmt.Println(err)
	err2 := processEvenNumber(42) // succeeds
	fmt.Println(err2)
}
Output:

failed to process number (1): not even
<nil>

func Expunge

func Expunge(exception *error, format string, a ...interface{})

Expunge rewrites an error message, when an error is non-nil. It removes potentially sensitive details from the exception and makes it less verbose, removing text of wrapped errors. It relies on text conventions, see Redact().

Details are redacted from the exception passed in. The format and arguments passed in become part of the new error message. Nothing is redacted from those additional details.

This is intended to be invoked as a deferred function, as a convenient way to remove details from an error immediately before returning it from a public API.

Example
package main

import (
	"fmt"

	"github.com/memsql/errors"
)

func main() {
	// mustBeEven is at odds with odds.
	mustBeEven := func(input int) error {
		if (input % 2) == 1 {
			return errors.New("not even")
		}
		return nil
	}

	// processEvenNumber uses errors.Expand() to add details when returning non-nil error.
	processEvenNumber := func(input int) (err error) {
		defer errors.Expand(&err, "failed to process number (%d)", input)

		return mustBeEven(input)
	}

	// ProcessSecretNumber uses errors.Expunge to control details presented when returning non-nil error from a
	// public-facing API.
	processSecretNumber := func(input int) (err error) {
		//
		// Expunge() redacts details from non-nil err, leaves nil error as-is.
		//
		defer errors.Expunge(&err, "unexpected error")

		return processEvenNumber(input)
	}

	err := processSecretNumber(1) // fails because not even
	fmt.Println(err)

	err2 := processSecretNumber(42) // succeeds
	fmt.Println(err2)
}
Output:

unexpected error: failed to process number
<nil>

func ExpungeOnce

func ExpungeOnce(exception *error, format string, a ...interface{})

ExpungeOnce behaves like Expunge(), except that it leaves an exception as-is if the it has already been expunged.

This is useful when a function has multiple stages, during which different details should be included in the exception. Deferred functions are called in a specific order, and only one deferred ExpungeOnce() will affect the error. In other words, the last reached deferred ExpungeOnce() will determine the final error message.

Example
package main

import (
	"fmt"

	"github.com/memsql/errors"
)

func main() {
	// mustBeEven is at odds with odds.
	mustBeEven := func(input int) error {
		if (input % 2) == 1 {
			return errors.New("not even")
		}
		return nil
	}

	// processEvenNumber uses errors.Expand() to add details when returning non-nil error.
	processEvenNumber := func(input int) (err error) {
		defer errors.Expand(&err, "failed to process number (%d)", input)

		return mustBeEven(input)
	}

	// ProcessSecretNumber uses errors.Expunge to control details presented when returning non-nil error from a
	// public-facing API.
	processSecretNumber := func(input int) (err error) {
		//
		// ExpungeOnce() redacts details from non-nil err, leaves nil error as-is. When multiple ExpungeOnce() are
		// deferred, only one will affect the error.
		//
		defer errors.ExpungeOnce(&err, "secret number must be even")
		if err = processEvenNumber(input); err != nil {
			return err
		}

		defer errors.ExpungeOnce(&err, "secret number must be divisible by 4")
		return processEvenNumber(input / 2)
	}

	err := processSecretNumber(1) // fails because not even
	fmt.Println(err)

	err2 := processSecretNumber(42) // fails because not divisible by 4
	fmt.Println(err2)
}
Output:

secret number must be even: failed to process number
secret number must be divisible by 4: failed to process number

func FromPanic

func FromPanic(in interface{}) (exception error)

FromPanic produces an error when passed non-nil input. It accepts input of any type, in order to support being invoked with what is returned from recover().

defer func() {
   if err = errors.FromPanic(recover()); err != nil { ... }
}()

func New

func New(text string) error

New emulates the behavior of stdlib's errors.New(), and includes a stack trace with the error.

func RegisterCapture

func RegisterCapture(name CaptureProvider, handler CaptureFunc)

RegisterCapture adds a handler to the set that will be invoked each time an error is captured.

func UnregisterCapture

func UnregisterCapture(name CaptureProvider)

func Walk

func Walk(exception error, f func(error) bool)

Walk visits each error in a tree of errors wrapping other errors.

The handler func, f, takes in the error being visited. The walk continues if the handler returns true, and does not continue if the handler returns false.

func WithStack

func WithStack(err error) error

WithStack produces an error that includes a stack trace. Note that if the wrapped error already has a stack, that error is returned without modification. Thus only the first call to WithStack will produce a stack trace. In other words when an error is wrapped multiple times, it is the stack of the earliest wrapped error which has priority.

To add information to an error message, use Errorf() instead. This function is provided to add a stack trace to a third-party error without otherwise altering the error text.

func Wrap

func Wrap(exception error, message string) error

Wrap returns nil when the exception passed in is nil; otherwise, it returns an error with message text that wraps exception.

This function provides an alternative to

err = f()
if err != nil {
  return errors.Errorf("failed objective: %w", err)
}
return nil

can be written as

return errors.Wrap(f(), "failed objective")

func Wrapf

func Wrapf(exception error, format string, a ...interface{}) error

Wrapf returns nil when the exception passed in is nil; otherwise, it produces text based on the format string and arguments, and returns an error with that text that wraps the exception.

See Wrap() for rationale.

Types

type CaptureFunc

type CaptureFunc func(err error, arg ...interface{}) CaptureID

CaptureFunc is a handler invoked to capture an error.

Any mechanism that can save an error may provide a capture handler. For example sentry, a log, etc... The CaptureID returned should be a way to find the error among other errors captured by the mechanism.

type CaptureID

type CaptureID string // may be a URL or any string that allows a captured error to be looked up

func LogCapture

func LogCapture(exception error, arg ...interface{}) CaptureID

LogCapture is a simple capture handler that writes exception to log.

type CaptureProvider

type CaptureProvider string // i.e. "sentry"

type Captured

type Captured struct {
	// contains filtered or unexported fields
}

Captured marks and wraps an error that has been "captured", meaning it has been logged verbosely or stored in a way that can be looked up later.

func (*Captured) Format

func (e *Captured) Format(f fmt.State, c rune)

Format produces a message with capture ID appended. The intention is to allow the captured details to be looked up, by engineers with access to the capture mechanism.

func (*Captured) ID

func (e *Captured) ID(provider CaptureProvider) CaptureID

ID returns an identifier created when a capture handler recorded the error.

func (*Captured) Unwrap

func (e *Captured) Unwrap() error

Unwrap allows errors.Unwrap to return the parent error.

type Error

type Error struct {
	// contains filtered or unexported fields
}

Error implements Go's error interface; and can format verbose messages, including stack traces.

func Errorf

func Errorf(format string, a ...interface{}) *Error

Errorf produces an error with a formatted message including dynamic arguments.

Callers are encouraged to include all relevant arguments in a well-constructed error message. Most arguments are stored to provide additional metadata if the error is later captured. Arguments will not be stored when apparently redundant, for example a wrapped error or string included in error text.

func (*Error) Format

func (e *Error) Format(f fmt.State, c rune)

Format is implemented to produce the most verbose version of the error message, in particular when "%+v" is the format string.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap allows errors.Unwrap to return the parent error.

type Public

type Public struct {
	// contains filtered or unexported fields
}

func Redact

func Redact(err error) Public

Redact removes potential sensitive details from an error, making the message safe to display to an unprivileged user.

Redact removes content in parenthesis. That is, it expects only errors that follow the convention that potentially sensitive information appears in parentheses. Also that errors are relatively simple, i.e. without nested parentheses.

func (Public) Error

func (e Public) Error() string

func (Public) Unwrap

func (e Public) Unwrap() error

type StackTrace

type StackTrace = pkgerrors.StackTrace

type StackTracer

type StackTracer interface {
	StackTrace() pkgerrors.StackTrace
}

StackTracer is exported so that external packages can detect whether a err has stack trace associated.

type String

type String string

String provides a simple mechanism to define const errors. This enable packages to export simple errors using

const ErrNoDroids = errors.String("these are not the droids you're looking for")

func (String) Error

func (s String) Error() string

func (String) Errorf

func (s String) Errorf(format string, a ...interface{}) error

Errorf returns an error which satisfies errors.Is(ex, s), without necessarily containing the text of string s.

func (String) Wrap added in v0.2.0

func (s String) Wrap(err error) error

Wrap returns nil when passed nil, otherwise it returns an error such that errors.Is returns true when compared to the String error or the passed in error. It leaves the error text unchanged.

type Throttle

type Throttle struct {
	Scope     string
	Threshold int32
	// contains filtered or unexported fields
}

A Throttle will alert, until threshold is reached. After threshold is reached, errors are no longer alerted. See Throttle.Alert() for additional details. A throttle allows you to capture the first error(s) encountered on a given line of code, but not subsequent errors.

Use a throttle when you believe that an error warrants investigation if it ever occurs, but it is not necessary to capture every time it occurs.

The throttle is not persisted across restarts, so errors will be captured for each replica of a service and each time a replica is restarted. So, if you specify a Threshold of one, you might see two captures if the service has two replicas, or four after those replicas have restarted, etc.

func (*Throttle) Alert

func (t *Throttle) Alert(exception error) error

Alert will capture an exception identically to errors.Alert, until some threshold number of errors has been alerted. After that threshold amount, subsequent errors are returned without capture.

The goal is to log and capture errors, if they occur; while not capturing so many that noise exceeds signal in logs and sentry.

func (*Throttle) Alertf

func (t *Throttle) Alertf(format string, a ...interface{}) error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL