serrors

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Nov 14, 2022 License: Apache-2.0 Imports: 8 Imported by: 7

README

Go Report Card

serrors

serrors is a collection of utitilies for creating and working with errors. These include automatic stack traces based on ideas from https://github.com/pkg/errors, immutable package-level sentinel errors, based on ideas from Dave Cheney, and the ability to combine HTTP statuses with errors directly.

Stack Traces

One of the biggest differences between errors in Go and exceptions in other languages is that you don't get a stack trace with a Go error. serrors fixes this limitation.

Creating Stack Traces

The serrors.WithStack function takes an existing error and wraps it with a stack trace. This is meant for adapting errors returned by the standard library or third-party libraries when you have no other context to provide. If an error is passed to serrors.WithStack that already has an error that implements the serrors.StackTracer interface in its unwrap chain, the passed-in error is returned. If a nil error is passed to serrors.WithStack, nil is returned. These two rules make it possible to write the following code and not worry if there's already a stack trace (or no error) stored in err:

func DoSomething(input string) (string, error) {
    result, err := ThingToCall(input)
    return result, serrors.WithStack(err)
}

If you want to wrap an existing error with your own contextual information, use serrors.Errorf. This works exactly like fmt.Errorf, only it wraps the passed-in error with a stack trace as well:

func DoSomething(input string) (string, error) {
    result, err := ThingToCall(input)
    if err != nil {
        err = serrors.Errorf("calling ThingToCall: %w", err)
    }
    return result, err
}

If there's an error in the unwrap chain that implements the serrors.StackTracer interface, serrors.Errorf preserves the existing trace information.

If you are creating a new error that's only a string, use serrors.New. This is a drop-in replacement for errors.New:

func DoSomething(input string) (string, error) {
    if input == "" {
        return "", serrors.New("cannot supply an empty string to DoSomething")
    }
    result, err := ThingToCall(input)
    return result, serrors.WithStack(err)
}
Using Stack Traces

Once you have an error in your unwrap chain with a stack trace, there are three ways to get the trace back:

serrors.Trace

You can use the serrors.Trace function to get a []string that contains each line of the stack trace:

s := serrors.New("This is a stack trace error")
callStack, err := serrors.Trace(s, serrors.StandardFormat)
fmt.Println(callStack)

serrors.Trace takes two parameters. The first is the error, and the second is a text.Template. There are two default templates defined. serrors.PanicFormat produces an output that resembles stack traces produced by the output of a panic, while serrors.StandardFormat provides a condensed single-line output.

If you want to write your own template, there are three valid variables:

  • .Function (for the function name),
  • .File (for the file path and name)
  • .Line (for the line number).

If you supply an error that doesn't have an serrors.StackTracer in its unwrap chain, nil is returned for both the slice of strings and the error. If an invalid template is supplied, nil is returned for the slice and the error is returned (wrapped in its own stack trace). Otherwise, the stack trace is returned as a slice of strings along with a nil error.

Note that by default, the file path will include the absolute path to the file on the machine that built the code. If you want to hide this path, build using the -trimpath flag.

errors.As

Errors with stack traces produced by serrors will implement the serrors.StackTracer interface. You can use errors.As to cast the error and call StackTrace() directly:

err := serrors.New("This is a stack trace error")

var stackTracer serrors.StackTracer
if errors.As(err, &stackTracker) {
  frames := stackTracer.StackTrace()
  // ...
}
Formatting Verb

Errors with stack traces also implement the fmt.Formatter interface, so the %+v formatting directive can be used:

err := serrors.New("this is a stack trace error")
fmt.Printf("%+v\n", err)

Unfortunately, fmt formatting does not unwrap errors, so if an error with a stack trace is wrapped using a non-serrors utility function such as fmt.Errorf, the stack trace will not be printed this way. For that reason, in situations where it is critical to ensure that the stack trace is recovered, one of the other two methods should be preferred.

Sentinel

This error is based on the error type described by Dave Cheney in his blog post Constant Time.

For some Go errors, you only need a single value, defined at the package level. These errors are called sentinel errors, because they have a single value that you are supposed to check against. One example in the standard library is the io.EOF error that's returned when you get to the end of a file:

var EOF = errors.New("EOF")

There is one serious drawback to this declaration: it's a var. Technically, it could be changed by any other package, which would break == and errors.Is comparisons. This is one reason why it is considered bad form to use a variable at the package level.

It would be better if you used a const to declare a sentinel error. Unfortunately, you can't use a const declaration with errors.New because const declarations must be composed of constants, constant literals, and operators, and errors.New is a function. To work around this, serrors defines a type called Sentinel. This allows you to write:

const EOF = serrors.Sentinel("EOF")

This looks like a function call, but it's actually a (constant) string being typecast to an serrors.Sentinel type that meets the error interface. Errors of type serrors.Sentinel work with == and errors.Is. The following test passes:

func TestSentinel(t *testing.T) {
  const msg = "This is a constant error"
  const s = serrors.Sentinel(msg)
  if s.Error() != msg {
    t.Errorf("Expected `%s`, got `%s`", msg, s.Error())
  }

  err := s
  if err != s {
    t.Errorf("should be the same")
  }
  if !errors.Is(err, s) {
    t.Errorf("should be the same")
  }
  err2 := serrors.Errorf("Wrapping error: %w", s)
  if !errors.Is(err2, s) {
    t.Errorf("should be there")
  }
}

Since a sentinel is immutable, it can't contain stack trace information. When you return a sentinel error, the best practice is to wrap it in an serrors.WithStack call so that stack information is captured. Wrapped sentinel errors must be compared using errors.Is.

const NotPositive = serrors.Sentinel("only positive ints are supported")

func DoThing(i int) (int, error) {
  if i <= 0 {
    return 0, serrors.WithStack(NotPositive)
  }
  return i * 2, nil
}

The only limitation to sentinels is that the string that's being typecast needs to be a constant expression if you are assigning it to a const. You can, of course, use serrors.Sentinel with a var, but that provides no additional functionality over an errors.New.

Status Errors

Not all errors are equal; different errors mean different things. Some indicate a bug in the server, while others indicate bad data being passed in. HTTP status codes are used to indicate this at the web API tier, but there isn't an easy way to pass this information back to that layer.

The serrors package includes three helpers to attach a status code directly to an error:

  • serrors.WithStatus takes an existing and attaches a status code.
  • serrors.NewStatusError creates a new error from a string.
  • serrors.NewStatusErrorf creates an error with fmt.Errorf semantics.

For convenience, all helpers wrap errors with stack traces as well.

The API allows an int to be passed in as a status code. By convention, these should use HTTP status codes exclusively. Any other status codes run the risk of violating expectations across application boundaries.

In order to retrieve the status code from an error, you can use errors.As:

func HandleRequest(w http.ResponseWriter, r *http.Request) {
    err := ThingToCall()
    var sc serrors.StatusCoder
    switch {
    case errors.As(err, &sc):
        w.WriteHeader(sc.StatusCode())
    case err != nil:
        w.WriteHeader(http.StatusInternalServerError)
    default:
        w.WriteHeader(http.StatusOK)
    }
}

Testing serrors

The tests for serrors require you to run go test with the -trimpath flag:

go test -trimpath ./...

Documentation

Index

Constants

This section is empty.

Variables

View Source
var PanicFormat = template.Must(template.New("standardFormat").Parse("{{.Function}}\n\t{{.File}}:{{.Line}}"))

PanicFormat is a template resembling the output of a `panic` used to convert a *runtime.Frame to a string. Each entry is formatted as:

FUNCTION_NAME
	FILE_NAME:LINE_NUMBER
View Source
var StandardFormat = template.Must(template.New("standardFormat").Parse("{{.Function}} ({{.File}}:{{.Line}})"))

StandardFormat is a one-line template used to convert a *runtime.Frame to a string. Each entry is formatted as:

FUNCTION_NAME (FILE_NAME:LINE_NUMBER)

Functions

func Errorf

func Errorf(format string, vals ...interface{}) error

Errorf wraps the error returned by fmt.Errorf in a StackErr. If there is an existing StackTracer in the unwrap chain, its stack trace will be preserved.

func New

func New(msg string) error

New builds a StackErr out of a string.

func NewFromStatus added in v0.3.1

func NewFromStatus(code int) error

NewFromStatus creates a new StatusError with the given status code and the HTTP status text from the standard library.

func NewStatusError added in v0.3.0

func NewStatusError(code int, msg string) error

NewStatusError returns an error that bundles an HTTP status code. The stack trace will be captured.

func NewStatusErrorf added in v0.3.0

func NewStatusErrorf(code int, format string, a ...interface{}) error

NewStatusErrorf returns an error that bundles an HTTP status code, sharing fmt.Errorf semantics. The stack trace will be captured if not already present.

func Trace

func Trace(e error, t *template.Template) ([]string, error)

Trace returns the stack trace information as a slice of strings formatted using the provided Go template. The valid fields in the template are Function, File, and Line. See StandardFormat for an example.

func WithStack

func WithStack(err error) error

WithStack takes in an error and returns an error wrapped in a StackErr with the location where an error was first created or returned from third-party code. If there is already an error in the error chain that exposes a stacktrace via the StackTrace() method, WithStack returns the passed-in error. If a nil error is passed in, nil is returned.

func WithStatus added in v0.3.0

func WithStatus(code int, err error) error

WithStatus bundles an error with an HTTP status code. The stack trace will be captured if not already present.

Types

type Sentinel

type Sentinel string

Sentinel is a way to turn a constant string into an error. It allows you to safely declare a package-level error so that it can't be accidentally modified to refer to a different value.

func (Sentinel) Error

func (s Sentinel) Error() string

Error is the marker interface for an error. This converts a Sentinel into a string for output.

type StackTracer

type StackTracer interface {
	StackTrace() *runtime.Frames
}

StackTracer defines an interface that's met by an error that returns a stacktrace. This is intended to be used by errors that capture the stacktrace to the source of the error. Each invocation of StackTrace() must return a new instance of *runtime.Frames, so that this method can be invoked more than once (runtime.Frames uses internal iteration and has no way to reset the iterator).

type StatusCoder added in v0.3.0

type StatusCoder interface {
	StatusCode() int
}

StatusCoder represents something that returns a status code.

Jump to

Keyboard shortcuts

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