merry

package module
v1.8.0 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2023 License: MIT Imports: 3 Imported by: 230

README

merry Build GoDoc Go Report Card

Add context to errors, including automatic stack capture, cause chains, HTTP status code, user messages, and arbitrary values.

The package is largely based on http://github.com/go-errors/errors, with additional inspiration from https://github.com/go-errgo/errgo and https://github.com/amattn/deeperror.

V2

github.com/ansel1/merry/v2 now replaces v1. v1 will continue to be supported. v1 has been re-implemented in terms of v2, and the two packages can be used together and interchangeably.

There are some small enhancements and changes to v1 with the introduction of v2:

  • err.Error() now always just prints out the basic error message. It no longer prints out details, user message, or cause. VerboseDefault() and SetVerboseDefault() no longer have any effect. To print more detailed error information, you must use fmt:

      // print err message and cause chain
      fmt.Printf("%v", err)    // %s works too
    
      // print details, same as Details(err)
      fmt.Printf("%v+", err) 
    
  • MaxStackDepth is no longer supported. Setting it has no effect. It has been replaced with GetMaxStackDepth() and SetMaxStackDepth(), which delegate to corresponding v2 functions.

  • New, Errorf, Wrap, and WrapSkipping now accept v2.Wrapper arguments, allowing a mixture of v1's fluent API style and v2's option-func API style.

  • Compatibility with other error wrapping libraries is improved. All functions which extract a value from an error will now search the entire chain of errors, even if errors created by other libraries are inserted in the middle of the chain, so long as those errors implement Unwrap().

Installation

go get github.com/ansel1/merry

Features

Merry errors work a lot like google's golang.org/x/net/context package. Merry errors wrap normal errors with a context of key/value pairs. Like contexts, merry errors are immutable: adding a key/value to an error always creates a new error which wraps the original.

merry comes with built-in support for adding information to errors:

  • stacktraces
  • overriding the error message
  • HTTP status codes
  • End user error messages

You can also add your own additional information.

The stack capturing feature can be turned off for better performance, though it's pretty fast. Benchmarks on an 2017 MacBook Pro, with go 1.10:

BenchmarkNew_withStackCapture-8      	 2000000	       749 ns/op
BenchmarkNew_withoutStackCapture-8   	20000000	        64.1 ns/op

Details

  • Support for go 2's errors.Is and errors.As functions

  • New errors have a stacktrace captured where they are created

  • Add a stacktrace to existing errors (captured where they are wrapped)

    err := lib.Read()
    return merry.Wrap(err)  // no-op if err is already merry
    
  • Add a stacktrace to a sentinel error

    var ParseError = merry.New("parse error")
    
    func Parse() error {
    	// ...
        return ParseError.Here() // captures a stacktrace here
    }
    
  • The golang idiom for testing errors against sentinel values or type checking them doesn't work with merry errors, since they are wrapped. Use Is() for sentinel value checks, or the new go 2 errors.As() function for testing error types.

    err := Parse()
    
    // sentinel value check
    if merry.Is(err, ParseError) {
       // ...
    }
    
    // type check
    if serr, ok := merry.Unwrap(err).(*SyntaxError); ok {
      // ...
    }
    
    // these only work in go1.13
    
    // sentinel value check
    if errors.Is(err, ParseError) {}
    
    // type check
    var serr *SyntaxError
    if errors.As(err, &serr) {}
    
  • Add to the message on an error.

    err := merry.Prepend(ParseError, "reading config").Append("bad input")
    fmt.Println(err.Error()) // reading config: parse error: bad input
    
  • Hierarchies of errors

    var ParseError = merry.New("Parse error")
    var InvalidCharSet = merry.WithMessage(ParseError, "Invalid char set")
    var InvalidSyntax = merry.WithMessage(ParseError, "Invalid syntax")
    
    func Parse(s string) error {
        // use chainable methods to add context
        return InvalidCharSet.Here().WithMessagef("Invalid char set: %s", "UTF-8")
        // or functions
        // return merry.WithMessagef(merry.Here(InvalidCharSet), "Invalid char set: %s", "UTF-8")
    }
    
    func Check() {
        err := Parse("fields")
        merry.Is(err, ParseError) // yup
        merry.Is(err, InvalidCharSet) // yup
        merry.Is(err, InvalidSyntax) // nope
    }
    
  • Add an HTTP status code

    merry.HTTPCode(errors.New("regular error")) // 500
    merry.HTTPCode(merry.New("merry error").WithHTTPCode(404)) // 404
    
  • Set an alternate error message for end users

    e := merry.New("crash").WithUserMessage("nothing to see here")
    merry.UserMessage(e)  // returns "nothing to see here"
    
  • Functions for printing error details

    err := merry.New("boom")
    m := merry.Stacktrace(err) // just the stacktrace
    m = merry.Details(err) // error message and stacktrace
    fmt.Sprintf("%+v", err) == merry.Details(err) // errors implement fmt.Formatter
    
  • Add your own context info

    err := merry.New("boom").WithValue("explosive", "black powder")
    

Basic Usage

The package contains functions for creating new errors with stacks, or adding a stack to error instances. Functions with add context (e.g. WithValue()) work on any error, and will automatically convert them to merry errors (with a stack) if necessary.

Capturing the stack can be globally disabled with SetStackCaptureEnabled(false)

Functions which get context values from errors also accept error, and will return default values if the error is not merry, or doesn't have that key attached.

All the functions which create or attach context return concrete instances of *Error. *Error implements methods to add context to the error (they mirror the functions and do the same thing). They allow for a chainable syntax for adding context.

Example:

package main

import (
    "github.com/ansel1/merry"
    "errors"
)

var InvalidInputs = errors.New("Input is invalid")

func main() {
    // create a new error, with a stacktrace attached
    err := merry.New("bad stuff happened")
    
    // create a new error with format string, like fmt.Errorf
    err = merry.Errorf("bad input: %v", os.Args)
    
    // capture a fresh stacktrace from this callsite
    err = merry.Here(InvalidInputs)
    
    // Make err merry if it wasn't already.  The stacktrace will be captured here if the
    // error didn't already have one.  Also useful to cast to *Error 
    err = merry.Wrap(err, 0)

    // override the original error's message
    err.WithMessagef("Input is invalid: %v", os.Args)
    
    // Use Is to compare errors against values, which is a common golang idiom
    merry.Is(err, InvalidInputs) // will be true
    
    // associated an http code
    err.WithHTTPCode(400)
    
    perr := parser.Parse("blah")
    err = Wrap(perr, 0)
    // Get the original error back
    merry.Unwrap(err) == perr  // will be true
    
    // Print the error to a string, with the stacktrace, if it has one
    s := merry.Details(err)
    
    // Just print the stacktrace (empty string if err is not a RichError)
    s := merry.Stacktrace(err)

    // Get the location of the error (the first line in the stacktrace)
    file, line := merry.Location(err)
    
    // Get an HTTP status code for an error.  Defaults to 500 for non-nil errors, and 200 if err is nil.
    code := merry.HTTPCode(err)
    
}

See inline docs for more details.

Plugs

License

This package is licensed under the MIT license, see LICENSE.MIT for details.

Documentation

Overview

Package merry provides enriched golang errors, with stacktraces

merry creates errors with stacktraces, and can augment those errors with additional information.

When you create a new merry error, or wrap an existing error in a merry error, merry attaches a stacktrace to the error:

err := merry.New("an error occurred")

err has a stacktrace attached. Alternately, you can wrap existing errors. merry will attach a stacktrace at the point of wrapping:

_, err := ioutil.ReadAll(r)
if err != nil {
    return merry.Wrap(err)
}

Capturing the stack can be globally disabled with `SetStackCaptureEnabled(false)`. Wrapping is idempotent: Wrap will only attach a stacktrace if the error doesn't already have one.

Wrap() is the simplest way to attach a stacktrace to an error, but other functions can be used instead, with both add a stacktrace, and augment or modify the error. For example, Prepend() modifies the error's message (and also attaches a stacktrace):

_, err := ioutil.ReadAll(r)
if err != nil {
    return merry.Prepend(err, "reading from conn failed")
    // err.Error() would read something like "reading from conn failed: timeout"
}

See the other package functions for other ways to augment or modify errors, such as Append, WithUserMessage, WithHTTPCode, WithValue, etc. These functions all return a merry.Error interface, which has methods which mirror the package level functions, to allow simple chaining:

return merry.New("object not found").WithHTTPCode(404)

Here

Wrap will not take a new stacktrace if an error already has one attached. Here will create a new error which replaces the stacktrace with a new one:

var ErrOverflow = merry.New("overflowed")

func Read() error {
    // ...
    return merry.Here(ErrOverflow)
}

Is

The go idiom of exporting package-level error variables for comparison to errors returned by the package is broken by merry. For example:

_, err := io.ReadAll(r)
if err == io.EOF {
    // ...
}

If the error returned was a merry error, the equality comparison would always fail, because merry augments errors by wrapping them in layers. To compensate for this, merry has the Is() function.

if merry.Is(err, io.EOF) {

Is() will unwrap the err and compare each layer to the second argument.

Cause

You can add a cause to an error:

if err == io.EOF {
    err = merry.New("reading failed"), err)
    fmt.Println(err.Error()) // reading failed: EOF
}

Cause(error) will return the cause of the argument. RootCause(error) returns the innermost cause. Is(err1, err2) is cause aware, and will return true if err2 is a cause (anywhere in the causal change) of err1.

Formatting and printing

To obtain an error's stacktrace, call Stack(). To get other information about the site of the error, or print the error's stacktrace, see Location(), SourceLine(), Stacktrace(), and Details().

merry errors also implement the fmt.Formatter interface. errors support the following fmt flags:

%+v   print the equivalent of Details(err), which includes the user message, full stacktrace,
      and recursively prints the details of the cause chain.

Index

Constants

This section is empty.

Variables

View Source
var MaxStackDepth = 50

MaxStackDepth is no longer used. It remains here for backward compatibility. deprecated: See Set/GetMaxStackDepth.

Functions

func Cause added in v1.2.0

func Cause(err error) error

Cause returns the cause of the argument. If e is nil, or has no cause, nil is returned.

func Details

func Details(err error) string

Details returns e.Error(), e's stacktrace, and any additional details which have be registered with RegisterDetail. User message and HTTP code are already registered.

The details of each error in e's cause chain will also be printed.

func GetMaxStackDepth added in v1.6.0

func GetMaxStackDepth() int

GetMaxStackDepth returns the number of frames captured in stacks.

func HTTPCode

func HTTPCode(err error) int

HTTPCode converts an error to an http status code. All errors map to 500, unless the error has an http code attached. If e is nil, returns 200.

func Is

func Is(e error, originals ...error) bool

Is is equivalent to errors.Is, but tests against multiple targets.

merry.Is(err1, err2, err3) == errors.Is(err1, err2) || errors.Is(err1, err3)

func Location

func Location(err error) (file string, line int)

Location returns zero values if e has no stacktrace

func Message

func Message(err error) string

Message returns just returns err.Error(). It is here for historical reasons.

func RegisterDetail added in v1.3.0

func RegisterDetail(label string, key interface{})

RegisterDetail registers an error property key in a global registry, with a label. The registry is used by the Details() function. Registered error properties will be included in Details() output, if the value of that error property is not nil. For example:

err := New("boom")
err = err.WithValue(colorKey, "red")
fmt.Println(Details(err))

// Output:
// boom
//
// <stacktrace>

RegisterDetail("Color", colorKey)
fmt.Println(Details(err))

// Output:
// boom
// Color: red
//
// <stacktrace>

Error property keys are typically not exported by the packages which define them. Packages instead export functions which let callers access that property. It's therefore up to the package to register those properties which would make sense to include in the Details() output. In other words, it's up to the author of the package which generates the errors to publish printable error details, not the callers of the package.

func RegisteredDetails added in v1.6.0

func RegisteredDetails(err error) map[string]interface{}

RegisteredDetails extracts details registered with RegisterDetailFunc from an error, and returns them as a map. Values may be nil.

If err is nil or there are no registered details, nil is returned.

func RootCause added in v1.2.0

func RootCause(err error) error

RootCause returns the innermost cause of the argument (i.e. the last error in the cause chain)

func SetMaxStackDepth added in v1.6.0

func SetMaxStackDepth(depth int)

SetMaxStackDepth sets the MaxStackDepth.

func SetStackCaptureEnabled

func SetStackCaptureEnabled(enabled bool)

SetStackCaptureEnabled sets stack capturing globally. Disabling stack capture can increase performance

func SetVerboseDefault

func SetVerboseDefault(bool)

SetVerboseDefault used to control the behavior of the Error() function on errors processed by this package. Error() now always just returns the error's message. This setting no longer has any effect. deprecated: To print the details of an error, use Details(err), or format the error with the verbose flag: fmt.Sprintf("%+v", err)

func SourceLine

func SourceLine(err error) string

SourceLine returns the string representation of Location's result or an empty string if there's no stracktrace.

func Stack

func Stack(err error) []uintptr

Stack returns the stack attached to an error, or nil if one is not attached If e is nil, returns nil.

func StackCaptureEnabled

func StackCaptureEnabled() bool

StackCaptureEnabled returns whether stack capturing is enabled

func Stacktrace

func Stacktrace(err error) string

Stacktrace returns the error's stacktrace as a string formatted the same way as golangs runtime package. If e has no stacktrace, returns an empty string.

func Unwrap

func Unwrap(e error) error

Unwrap returns the innermost underlying error. This just calls errors.Unwrap() until if finds the deepest error. It isn't very useful, and only remains for historical purposes

deprecated: use errors.Is() or errors.As() instead.

func UserMessage

func UserMessage(err error) string

UserMessage returns the end-user safe message. Returns empty if not set. If e is nil, returns "".

func Value

func Value(err error, key interface{}) interface{}

Value returns the value for key, or nil if not set. If e is nil, returns nil.

func Values

func Values(err error) map[interface{}]interface{}

Values returns a map of all values attached to the error If a key has been attached multiple times, the map will contain the last value mapped If e is nil, returns nil.

func VerboseDefault

func VerboseDefault() bool

VerboseDefault no longer has any effect. deprecated: see SetVerboseDefault

Types

type Error

type Error interface {
	error
	Appendf(format string, args ...interface{}) Error
	Append(msg string) Error
	Prepend(msg string) Error
	Prependf(format string, args ...interface{}) Error
	WithMessage(msg string) Error
	WithMessagef(format string, args ...interface{}) Error
	WithUserMessage(msg string) Error
	WithUserMessagef(format string, args ...interface{}) Error
	WithValue(key, value interface{}) Error
	Here() Error
	WithStackSkipping(skip int) Error
	WithHTTPCode(code int) Error
	WithCause(err error) Error
	Cause() error
	fmt.Formatter
}

Error extends the standard golang `error` interface with functions for attachment additional data to the error

func Append

func Append(err error, msg string) Error

Append a message after the current error message, in the format "original: new". If e == nil, return nil.

func Appendf

func Appendf(err error, format string, args ...interface{}) Error

Appendf is the same as Append, but uses fmt.Sprintf().

func Errorf

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

Errorf creates a new error with a formatted message and a stack. The equivalent of golang's fmt.Errorf(). args can be format args, or v2 wrappers which will be applied to the error.

func Here

func Here(err error) Error

Here returns an error with a new stacktrace, at the call site of Here(). Useful when returning copies of exported package errors. If e is nil, returns nil.

func HereSkipping added in v1.1.0

func HereSkipping(err error, skip int) Error

HereSkipping returns an error with a new stacktrace, at the call site of HereSkipping() - skip frames.

func New

func New(msg string, wrappers ...v2.Wrapper) Error

New creates a new error, with a stack attached. The equivalent of golang's errors.New(). Accepts v2 wrappers to apply to the error.

func Prepend

func Prepend(err error, msg string) Error

Prepend a message before the current error message, in the format "new: original". If e == nil, return nil.

func Prependf

func Prependf(err error, format string, args ...interface{}) Error

Prependf is the same as Prepend, but uses fmt.Sprintf()

func UserError

func UserError(msg string) Error

UserError creates a new error with a message intended for display to an end user.

func UserErrorf

func UserErrorf(format string, args ...interface{}) Error

UserErrorf is like UserError, but uses fmt.Sprintf()

func WithCause added in v1.2.0

func WithCause(err error, cause error) Error

WithCause returns an error based on the first argument, with the cause set to the second argument. If e is nil, returns nil.

func WithHTTPCode

func WithHTTPCode(e error, code int) Error

WithHTTPCode returns an error with an http code attached. If e is nil, returns nil.

func WithMessage

func WithMessage(err error, msg string) Error

WithMessage returns an error with a new message. The resulting error's Error() method will return the new message. If e is nil, returns nil.

func WithMessagef

func WithMessagef(err error, format string, args ...interface{}) Error

WithMessagef is the same as WithMessage(), using fmt.Sprintf().

func WithUserMessage

func WithUserMessage(err error, msg string) Error

WithUserMessage adds a message which is suitable for end users to see. If e is nil, returns nil.

func WithUserMessagef

func WithUserMessagef(err error, format string, args ...interface{}) Error

WithUserMessagef is the same as WithMessage(), using fmt.Sprintf()

func WithValue

func WithValue(err error, key, value interface{}) Error

WithValue adds a context an error. If the key was already set on e, the new value will take precedence. If e is nil, returns nil.

func Wrap

func Wrap(err error, wrappers ...v2.Wrapper) Error

Wrap turns the argument into a merry.Error. If the argument already is a merry.Error, this is a no-op. If e == nil, return nil

func WrapSkipping

func WrapSkipping(err error, skip int, wrappers ...v2.Wrapper) Error

WrapSkipping turns the error arg into a merry.Error if the arg is not already a merry.Error. If e is nil, return nil. If a merry.Error is created by this call, the stack captured will skip `skip` frames (0 is the call site of `WrapSkipping()`)

Jump to

Keyboard shortcuts

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