-
Notifications
You must be signed in to change notification settings - Fork 94
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add errors package with optional stacktrace capturing (#431)
The goal of this package is to enable to capture stack traces when the error are being raised, which speeds up the development process, if enabled at compile time. To do so, this set of changes introduces an internal/errors package to handle errors with four functions: .New, .Errorf, .Is and .Unwrap. Albeit support for reflect has recently been added recently to Tinygo, the present set of changes does not depend on reflect in any way. If the code is compiled with the debug build tag, the stack trace will be captured when an error is created or wrapped (both with .New). Without that build tag, the errors packages falls back to simply comparing values and performs no wrapping at all.
- Loading branch information
Showing
5 changed files
with
514 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.