557 lines
14 KiB
Go
557 lines
14 KiB
Go
package merry
|
|
|
|
// The merry package augments standard golang errors with stacktraces
|
|
// and other context information.
|
|
//
|
|
// You can add any context information to an error with `e = merry.WithValue(e, "code", 12345)`
|
|
// You can retrieve that value with `v, _ := merry.Value(e, "code").(int)`
|
|
//
|
|
// Any error augmented like this will automatically get a stacktrace attached, if it doesn't have one
|
|
// already. If you just want to add the stacktrace, use `Wrap(e)`
|
|
//
|
|
// It also providers a way to override an error's message:
|
|
//
|
|
// var InvalidInputs = errors.New("Bad inputs")
|
|
//
|
|
// `Here()` captures a new stacktrace, and WithMessagef() sets a new error message:
|
|
//
|
|
// return merry.Here(InvalidInputs).WithMessagef("Bad inputs: %v", inputs)
|
|
//
|
|
// Errors are immutable. All functions and methods which add context return new errors.
|
|
// But errors can still be compared to the originals with `Is()`
|
|
//
|
|
// if merry.Is(err, InvalidInputs) {
|
|
//
|
|
// Functions which add context to errors have equivalent methods on *Error, to allow
|
|
// convenient chaining:
|
|
//
|
|
// return merry.New("Invalid body").WithHTTPCode(400)
|
|
//
|
|
// merry.Errors also implement fmt.Formatter, similar to github.com/pkg/errors.
|
|
//
|
|
// fmt.Sprintf("%+v", e) == merry.Details(e)
|
|
//
|
|
// pkg/errors Cause() interface is not implemented (yet).
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"runtime"
|
|
)
|
|
|
|
// MaxStackDepth is the maximum number of stackframes on any error.
|
|
var MaxStackDepth = 50
|
|
|
|
var captureStacks = true
|
|
var verbose = false
|
|
|
|
// StackCaptureEnabled returns whether stack capturing is enabled
|
|
func StackCaptureEnabled() bool {
|
|
return captureStacks
|
|
}
|
|
|
|
// SetStackCaptureEnabled sets stack capturing globally. Disabling stack capture can increase performance
|
|
func SetStackCaptureEnabled(enabled bool) {
|
|
captureStacks = enabled
|
|
}
|
|
|
|
// VerboseDefault returns the global default for verbose mode.
|
|
// When true, e.Error() == Details(e)
|
|
// When false, e.Error() == Message(e)
|
|
func VerboseDefault() bool {
|
|
return verbose
|
|
}
|
|
|
|
// SetVerboseDefault sets the global default for verbose mode.
|
|
// When true, e.Error() == Details(e)
|
|
// When false, e.Error() == Message(e)
|
|
func SetVerboseDefault(b bool) {
|
|
verbose = b
|
|
}
|
|
|
|
// Error extends the standard golang `error` interface with functions
|
|
// for attachment additional data to the 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
|
|
fmt.Formatter
|
|
}
|
|
|
|
// New creates a new error, with a stack attached. The equivalent of golang's errors.New()
|
|
func New(msg string) Error {
|
|
return WrapSkipping(errors.New(msg), 1)
|
|
}
|
|
|
|
// Errorf creates a new error with a formatted message and a stack. The equivalent of golang's fmt.Errorf()
|
|
func Errorf(format string, a ...interface{}) Error {
|
|
return WrapSkipping(fmt.Errorf(format, a...), 1)
|
|
}
|
|
|
|
// UserError creates a new error with a message intended for display to an
|
|
// end user.
|
|
func UserError(msg string) Error {
|
|
return WrapSkipping(errors.New(""), 1).WithUserMessage(msg)
|
|
}
|
|
|
|
// UserErrorf is like UserError, but uses fmt.Sprintf()
|
|
func UserErrorf(format string, a ...interface{}) Error {
|
|
return WrapSkipping(errors.New(""), 1).WithUserMessagef(format, a...)
|
|
}
|
|
|
|
// 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 Wrap(e error) Error {
|
|
return WrapSkipping(e, 1)
|
|
}
|
|
|
|
// 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()`)
|
|
func WrapSkipping(e error, skip int) Error {
|
|
switch e1 := e.(type) {
|
|
case nil:
|
|
return nil
|
|
case *merryErr:
|
|
return e1
|
|
default:
|
|
return &merryErr{
|
|
err: e,
|
|
key: stack,
|
|
value: captureStack(skip + 1),
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 WithValue(e error, key, value interface{}) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return WrapSkipping(e, 1).WithValue(key, value)
|
|
}
|
|
|
|
// Value returns the value for key, or nil if not set.
|
|
// If e is nil, returns nil.
|
|
func Value(e error, key interface{}) interface{} {
|
|
for {
|
|
switch m := e.(type) {
|
|
case nil:
|
|
return nil
|
|
case *merryErr:
|
|
if m.key == key {
|
|
return m.value
|
|
}
|
|
e = m.err
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 Values(e error) map[interface{}]interface{} {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
var values map[interface{}]interface{}
|
|
for {
|
|
w, ok := e.(*merryErr)
|
|
if !ok {
|
|
return values
|
|
}
|
|
if values == nil {
|
|
values = make(map[interface{}]interface{}, 1)
|
|
}
|
|
if _, ok := values[w.key]; !ok {
|
|
values[w.key] = w.value
|
|
}
|
|
e = w.err
|
|
}
|
|
}
|
|
|
|
// 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 Here(e error) Error {
|
|
switch m := e.(type) {
|
|
case nil:
|
|
return nil
|
|
case *merryErr:
|
|
// optimization: only capture the stack once, since its expensive
|
|
return m.WithStackSkipping(1)
|
|
default:
|
|
return WrapSkipping(e, 1)
|
|
}
|
|
}
|
|
|
|
// Stack returns the stack attached to an error, or nil if one is not attached
|
|
// If e is nil, returns nil.
|
|
func Stack(e error) []uintptr {
|
|
stack, _ := Value(e, stack).([]uintptr)
|
|
return stack
|
|
}
|
|
|
|
// WithHTTPCode returns an error with an http code attached.
|
|
// If e is nil, returns nil.
|
|
func WithHTTPCode(e error, code int) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return WrapSkipping(e, 1).WithHTTPCode(code)
|
|
}
|
|
|
|
// 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 HTTPCode(e error) int {
|
|
if e == nil {
|
|
return 200
|
|
}
|
|
code, _ := Value(e, httpCode).(int)
|
|
if code == 0 {
|
|
return 500
|
|
}
|
|
return code
|
|
}
|
|
|
|
// UserMessage returns the end-user safe message. Returns empty if not set.
|
|
// If e is nil, returns "".
|
|
func UserMessage(e error) string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
msg, _ := Value(e, userMessage).(string)
|
|
return msg
|
|
}
|
|
|
|
// Message returns just the error message. It is equivalent to
|
|
// Error() when Verbose is false.
|
|
// The behavior of Error() is (pseudo-code):
|
|
//
|
|
// if verbose
|
|
// Details(e)
|
|
// else
|
|
// Message(e) || UserMessage(e)
|
|
//
|
|
// If e is nil, returns "".
|
|
func Message(e error) string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
m, _ := Value(e, message).(string)
|
|
if m == "" {
|
|
return Unwrap(e).Error()
|
|
}
|
|
return m
|
|
}
|
|
|
|
// 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 WithMessage(e error, msg string) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return WrapSkipping(e, 1).WithValue(message, msg)
|
|
}
|
|
|
|
// WithMessagef is the same as WithMessage(), using fmt.Sprintf().
|
|
func WithMessagef(e error, format string, a ...interface{}) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return WrapSkipping(e, 1).WithMessagef(format, a...)
|
|
}
|
|
|
|
// WithUserMessage adds a message which is suitable for end users to see.
|
|
// If e is nil, returns nil.
|
|
func WithUserMessage(e error, msg string) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return WrapSkipping(e, 1).WithUserMessage(msg)
|
|
}
|
|
|
|
// WithUserMessagef is the same as WithMessage(), using fmt.Sprintf()
|
|
func WithUserMessagef(e error, format string, args ...interface{}) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return WrapSkipping(e, 1).WithUserMessagef(format, args...)
|
|
}
|
|
|
|
// Append a message after the current error message, in the format "original: new".
|
|
// If e == nil, return nil.
|
|
func Append(e error, msg string) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return WrapSkipping(e, 1).Append(msg)
|
|
}
|
|
|
|
// Appendf is the same as Append, but uses fmt.Sprintf().
|
|
func Appendf(e error, format string, args ...interface{}) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return WrapSkipping(e, 1).Appendf(format, args...)
|
|
}
|
|
|
|
// Prepend a message before the current error message, in the format "new: original".
|
|
// If e == nil, return nil.
|
|
func Prepend(e error, msg string) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return WrapSkipping(e, 1).Prepend(msg)
|
|
}
|
|
|
|
// Prependf is the same as Prepend, but uses fmt.Sprintf()
|
|
func Prependf(e error, format string, args ...interface{}) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return WrapSkipping(e, 1).Prependf(format, args...)
|
|
}
|
|
|
|
// Is checks whether e is equal to or wraps the original, at any depth.
|
|
// If e == nil, return false.
|
|
// This is useful if your package uses the common golang pattern of
|
|
// exported error constants. If your package exports an ErrEOF constant,
|
|
// which is initialized like this:
|
|
//
|
|
// var ErrEOF = errors.New("End of file error")
|
|
//
|
|
// ...and your user wants to compare an error returned by your package
|
|
// with ErrEOF:
|
|
//
|
|
// err := urpack.Read()
|
|
// if err == urpack.ErrEOF {
|
|
//
|
|
// ...the comparison will fail if the error has been wrapped by merry
|
|
// at some point. Replace the comparison with:
|
|
//
|
|
// if merry.Is(err, urpack.ErrEOF) {
|
|
//
|
|
func Is(e error, originals ...error) bool {
|
|
is := func(e, original error) bool {
|
|
for {
|
|
if e == original {
|
|
return true
|
|
}
|
|
if e == nil || original == nil {
|
|
return false
|
|
}
|
|
w, ok := e.(*merryErr)
|
|
if !ok {
|
|
return false
|
|
}
|
|
e = w.err
|
|
}
|
|
}
|
|
for _, o := range originals {
|
|
if is(e, o) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Unwrap returns the innermost underlying error.
|
|
// Only useful in advanced cases, like if you need to
|
|
// cast the underlying error to some type to get
|
|
// additional information from it.
|
|
// If e == nil, return nil.
|
|
func Unwrap(e error) error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
for {
|
|
w, ok := e.(*merryErr)
|
|
if !ok {
|
|
return e
|
|
}
|
|
e = w.err
|
|
}
|
|
}
|
|
|
|
func captureStack(skip int) []uintptr {
|
|
if !captureStacks {
|
|
return nil
|
|
}
|
|
stack := make([]uintptr, MaxStackDepth)
|
|
length := runtime.Callers(2+skip, stack[:])
|
|
return stack[:length]
|
|
}
|
|
|
|
type errorProperty string
|
|
|
|
const (
|
|
stack errorProperty = "stack"
|
|
message = "message"
|
|
httpCode = "http status code"
|
|
userMessage = "user message"
|
|
)
|
|
|
|
type merryErr struct {
|
|
err error
|
|
key, value interface{}
|
|
}
|
|
|
|
// Format implements fmt.Formatter
|
|
func (e *merryErr) Format(s fmt.State, verb rune) {
|
|
switch verb {
|
|
case 'v':
|
|
if s.Flag('+') {
|
|
io.WriteString(s, Details(e))
|
|
return
|
|
}
|
|
fallthrough
|
|
case 's':
|
|
io.WriteString(s, e.Error())
|
|
case 'q':
|
|
fmt.Fprintf(s, "%q", e.Error())
|
|
}
|
|
}
|
|
|
|
// make sure merryErr implements Error
|
|
var _ Error = (*merryErr)(nil)
|
|
|
|
// Error implements golang's error interface
|
|
// returns the message value if set, otherwise
|
|
// delegates to inner error
|
|
func (e *merryErr) Error() string {
|
|
if verbose {
|
|
return Details(e)
|
|
}
|
|
m := Message(e)
|
|
if m == "" {
|
|
return UserMessage(e)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// return a new error with additional context
|
|
func (e *merryErr) WithValue(key, value interface{}) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return &merryErr{
|
|
err: e,
|
|
key: key,
|
|
value: value,
|
|
}
|
|
}
|
|
|
|
// Shorthand for capturing a new stack trace
|
|
func (e *merryErr) Here() Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.WithStackSkipping(1)
|
|
}
|
|
|
|
// return a new error with a new stack capture
|
|
func (e *merryErr) WithStackSkipping(skip int) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return &merryErr{
|
|
err: e,
|
|
key: stack,
|
|
value: captureStack(skip + 1),
|
|
}
|
|
}
|
|
|
|
// return a new error with an http status code attached
|
|
func (e *merryErr) WithHTTPCode(code int) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.WithValue(httpCode, code)
|
|
}
|
|
|
|
// return a new error with a new message
|
|
func (e *merryErr) WithMessage(msg string) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.WithValue(message, msg)
|
|
}
|
|
|
|
// return a new error with a new formatted message
|
|
func (e *merryErr) WithMessagef(format string, a ...interface{}) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.WithMessage(fmt.Sprintf(format, a...))
|
|
}
|
|
|
|
// Add a message which is suitable for end users to see
|
|
func (e *merryErr) WithUserMessage(msg string) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.WithValue(userMessage, msg)
|
|
}
|
|
|
|
// Add a message which is suitable for end users to see
|
|
func (e *merryErr) WithUserMessagef(format string, args ...interface{}) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.WithUserMessage(fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// Append a message after the current error message, in the format "original: new"
|
|
func (e *merryErr) Append(msg string) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.WithMessagef("%s: %s", e.Error(), msg)
|
|
}
|
|
|
|
// Append a message after the current error message, in the format "original: new"
|
|
func (e *merryErr) Appendf(format string, args ...interface{}) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.Append(fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// Prepend a message before the current error message, in the format "new: original"
|
|
func (e *merryErr) Prepend(msg string) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.WithMessagef("%s: %s", msg, e.Error())
|
|
}
|
|
|
|
// Prepend a message before the current error message, in the format "new: original"
|
|
func (e *merryErr) Prependf(format string, args ...interface{}) Error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.Prepend(fmt.Sprintf(format, args...))
|
|
}
|