Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add errors package with optional stacktrace capturing #431

Merged
merged 1 commit into from
Aug 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions internal/errors/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package errors

import (
"bytes"
"reflect"
)

// Error wraps any error with a stacktrace, speeding up the development process.
// Such errors are only returned when the error package is compiled with the "debug" build tag.
type Error struct {
Err error
stack []uintptr
frames []StackFrame
}

// Error returns the underlying error's message.
func (err *Error) Error() string {
return err.Err.Error()
}

// Return the underlying error.
func (err *Error) Unwrap() error {
return err.Err
}

// Is returns true if err equals to the target or the error wrapped by the target.
func (err *Error) Is(target error) bool {
if err == target {
return true
}
if e, ok := target.(*Error); ok {
return err.Err == e.Err
}
return false
}

// Stack returns the callstack formatted the same way that go does
// in runtime/debug.Stack()
func (err *Error) Stack() []byte {
buf := bytes.Buffer{}
for _, frame := range err.StackFrames() {
buf.WriteString(frame.String())
}
return buf.Bytes()
}

// StackFrames returns an array of frames containing information about the
// stack.
func (err *Error) StackFrames() []StackFrame {
if err.frames == nil {
err.frames = make([]StackFrame, len(err.stack))

for i, pc := range err.stack {
err.frames[i] = NewStackFrame(pc)
}
}
return err.frames
}

// ErrorStack returns a string that contains both the
// error message and the callstack.
func (err *Error) ErrorStack() string {
return err.TypeName() + " " + err.Error() + "\n" + string(err.Stack())
}

// TypeName returns the type this error. e.g. *errors.stringError.
func (err *Error) TypeName() string {
return reflect.TypeOf(err.Err).String()
}
41 changes: 41 additions & 0 deletions internal/errors/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// +build !debug

// Package errors provides a simple API to create and compare errors. A debug version of this package
// exists, but captures stacktraces when error are created or wrapped. It is accessible through the
// the "debug" build tag.
package errors

import (
baseErrors "errors"

"github.com/genjidb/genji/internal/stringutil"
)

// New takes a string and returns a standard error.
func New(s string) error {
return baseErrors.New(s)
}

// Errorf creates an error out of a string. If %w is used to format an error, it will
// only wrap it by concatenation, the wrapped error won't be accessible directly and
// thus cannot be accessed through the Is or As functions from the standard error package.
func Errorf(format string, a ...interface{}) error {
return stringutil.Errorf(format, a...)
}

// Is performs a simple value comparison between err and original (==).
func Is(err, original error) bool {
return err == original
}

// Unwrap does nothing and just returns err.
// This function only acts differently when the debug version of this function is used.
func Unwrap(err error) error {
return err
}

// Wrap acts as the identity function, unless compiled with the debug tag.
// See the debug version of this package for more.
func Wrap(err error) error {
return err
}
108 changes: 108 additions & 0 deletions internal/errors/errors_debug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// +build debug

// Package errors provides a simple API to create and compare errors.
// It captures the stacktrace when an error is created or wrapped, which can be then be inspected for debugging purposes.
// This package, compiled with the "debug" build tag is only meant to ease development and should not be used otherwise.
package errors

import (
baseErrors "errors"
"runtime"

"github.com/genjidb/genji/internal/stringutil"
)

// New takes a string and returns a wrapped error that allows to inspect the stracktrace
// captured when this function was called.
func New(s string) error {
err := _new(s)
if len(err.stack) > 1 {
// Truncate the call to _new
err.stack = err.stack[1:]
}
return err
}

// Errorf creates an error that includes the stracktrace, out of a string. If %w is used to format an error, it will
// only wrap it by concatenation, the wrapped error won't be accessible directly and
// thus cannot be accessed through the Is or As functions from the standard error package.
func Errorf(format string, a ...interface{}) error {
return errorf(format, a...)
}

// Is performs a value comparison between err and the target, unwrapping them if necessary.
func Is(err, target error) bool {
if err == target {
return true
}
if e, ok := err.(*Error); ok {
if t, ok := target.(*Error); ok {
return e.Err == t.Err
} else {
return e.Err == target
}
}
if target, ok := target.(*Error); ok {
return err == target.Err
}
return false
}

// Unwrap returns the underlying error, or the error itself if err is not an *errors.Error.
func Unwrap(err error) error {
if err == nil {
return nil
}
if e, ok := err.(*Error); ok {
return e.Err
}
return err
}

// Wrap turns any error into an *errors.Error that also embeds the stacktrace.
// If the error is already wrapped, it will refresh its captured stacktrace,
// which is useful when the original context in which it was created has
// no meaning for the user.
func Wrap(e error) error {
if e == nil {
return nil
}
return wrap(e, 1)
}

// The maximum number of stackframes on any error.
var MaxStackDepth = 32

func _new(s string) *Error {
err := baseErrors.New(s)
return wrap(err, 1)
}

// wrap makes an Error from the given value. If that value is already an
// error then it will be used directly, if not, it will be passed to
// stringutil.Errorf("%v"). The skip parameter indicates how far up the stack
// to start the stacktrace. 0 is from the current call, 1 from its caller, etc.
func wrap(e interface{}, skip int) *Error {
if e == nil {
return nil
}
var err error
switch e := e.(type) {
case *Error:
err = e
case error:
err = e
default:
err = stringutil.Errorf("%v", e)
}
stack := make([]uintptr, MaxStackDepth)
length := runtime.Callers(2+skip, stack[:])
return &Error{
Err: err,
stack: stack[:length],
}
}

func errorf(format string, a ...interface{}) *Error {
return wrap(stringutil.Errorf(format, a...), 1)
}
Loading