289 lines
6.8 KiB
Go
289 lines
6.8 KiB
Go
package try
|
|
|
|
/*
|
|
try is a fork of lainio's err2 package
|
|
Copyright (c) 2019 Lainio, MIT License
|
|
https://github.com/lainio/err2
|
|
*/
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
)
|
|
|
|
// Err is an interface for an error with extended stack info.
|
|
type Err interface {
|
|
Error() string
|
|
Unwrap() error
|
|
CallStack() []call
|
|
CallStackString() string
|
|
PrintCallStack()
|
|
GetData() *tryErrData
|
|
Annotate(msg string)
|
|
}
|
|
|
|
// tryErr is an extended error struct.
|
|
type tryErr struct {
|
|
err error
|
|
annotations []string
|
|
callStack []call
|
|
}
|
|
|
|
// tryErrData is exported error data. Can be converted to JSON.
|
|
type tryErrData struct {
|
|
Msg string `json:"msg"`
|
|
Annotations []string `json:"annotations"`
|
|
CallStack []call `json:"call_stack"`
|
|
}
|
|
|
|
type call struct {
|
|
File string `json:"file"`
|
|
Function string `json:"fn"`
|
|
Line int `json:"l"`
|
|
}
|
|
|
|
// FromErr wraps a standard error into a try-compatible one with extended stack info.
|
|
func FromErr(err error) Err {
|
|
// If the error is already try-compatible, return
|
|
//nolint:errorlint
|
|
cterr, ok := err.(Err)
|
|
if ok {
|
|
return cterr
|
|
}
|
|
|
|
terr := &tryErr{err: err, callStack: []call{}}
|
|
|
|
pc, _, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
return terr
|
|
}
|
|
thisFnc := runtime.FuncForPC(pc)
|
|
if thisFnc == nil {
|
|
return terr
|
|
}
|
|
|
|
rexp := regexp.MustCompile(`.+\/`)
|
|
thisPkg := rexp.FindString(thisFnc.Name())
|
|
|
|
for i := 1; ; i++ {
|
|
pc, f, l, ok := runtime.Caller(i)
|
|
|
|
if !ok {
|
|
break
|
|
}
|
|
|
|
fnc := runtime.FuncForPC(pc).Name()
|
|
|
|
if strings.HasPrefix(fnc, thisPkg) && !strings.HasSuffix(f, "_test.go") {
|
|
continue
|
|
}
|
|
|
|
terr.callStack = append(terr.callStack, call{File: f, Function: fnc, Line: l})
|
|
}
|
|
|
|
return terr
|
|
}
|
|
|
|
// NewErr creates a new try-compatible error with extended stack info.
|
|
func NewErr(msg string) Err {
|
|
return FromErr(errors.New(msg))
|
|
}
|
|
|
|
// Error returns the standard go error message (initial message + annotations).
|
|
func (e *tryErr) Error() string {
|
|
msgs := e.annotations
|
|
msgs = append(msgs, e.err.Error())
|
|
return strings.Join(msgs, ": ")
|
|
}
|
|
|
|
// Unwrap returns the standard go error.
|
|
func (e *tryErr) Unwrap() error {
|
|
return e.err
|
|
}
|
|
|
|
// CallStack returns a slice of function calls leading to the error,
|
|
// with the most recent being first.
|
|
func (e *tryErr) CallStack() []call {
|
|
return e.callStack
|
|
}
|
|
|
|
// CallStackString outputs the call stack as a printable multiline string.
|
|
func (e *tryErr) CallStackString() string {
|
|
res := fmt.Sprintf("ERROR: %s\n", e.err.Error())
|
|
for _, msg := range e.annotations {
|
|
res += fmt.Sprintln(msg)
|
|
}
|
|
res += fmt.Sprintln("\nTraceback (most recent call first):")
|
|
for _, c := range e.callStack {
|
|
res += fmt.Sprintf("%s:%d\n", c.File, c.Line)
|
|
res += fmt.Sprintf(" %s\n", c.Function)
|
|
}
|
|
return res
|
|
}
|
|
|
|
// PrintCallStack prints the call stack to stderr.
|
|
func (e *tryErr) PrintCallStack() {
|
|
_, _ = fmt.Fprintln(os.Stderr, e.CallStackString())
|
|
}
|
|
|
|
// GetData returns a serializable error data object.
|
|
func (e *tryErr) GetData() *tryErrData {
|
|
return &tryErrData{
|
|
Msg: e.err.Error(),
|
|
CallStack: e.callStack,
|
|
Annotations: e.annotations,
|
|
}
|
|
}
|
|
|
|
// Annotate adds additional text to the error message.
|
|
func (e *tryErr) Annotate(msg string) {
|
|
e.annotations = append(e.annotations, msg)
|
|
}
|
|
|
|
// Empty is a helper method to handle errors of func() (string, error) functions.
|
|
func Empty(_ interface{}, err error) {
|
|
Check(err)
|
|
}
|
|
|
|
// Any is as similar as proposed Go2 Try macro, but it's a function and it
|
|
// returns slice of interfaces. It has quite big performance penalty when
|
|
// compared to Check function.
|
|
func Any(args ...interface{}) []interface{} {
|
|
check(args)
|
|
return args
|
|
}
|
|
|
|
// Check performs the error check for the given argument. If the err is nil,
|
|
// it does nothing. According the measurements, it's as fast as if err != nil
|
|
// {return err} on happy path.
|
|
func Check(err error) {
|
|
if err != nil {
|
|
panic(FromErr(err))
|
|
}
|
|
}
|
|
|
|
// Checks the error status of the last argument. It panics with "wrong
|
|
// signature" if the last calling parameter is not error. In case of error it
|
|
// delivers it by panicking.
|
|
func check(args []interface{}) {
|
|
argCount := len(args)
|
|
last := argCount - 1
|
|
if args[last] != nil {
|
|
err, ok := args[last].(error)
|
|
if !ok {
|
|
panic("wrong signature")
|
|
}
|
|
panic(FromErr(err))
|
|
}
|
|
}
|
|
|
|
// Handle is for adding an error handler to a function by defer. It's for
|
|
// functions returning errors them self. For those functions that doesn't
|
|
// return errors there is a Catch function. Note! The handler function f is
|
|
// called only when err != nil.
|
|
func Handle(err *Err, f func()) {
|
|
// We put real panic objects back and keep only those which are
|
|
// carrying our errors. We must also call all of the handlers in defer
|
|
// stack.
|
|
switch r := recover(); r.(type) {
|
|
case nil:
|
|
// Defers are in the stack and the first from the stack gets the
|
|
// opportunity to get panic object's error (below). We still must
|
|
// call handler functions to the rest of the handlers if there is
|
|
// an error.
|
|
if *err != nil {
|
|
f()
|
|
}
|
|
case Err:
|
|
// We or someone did transport this error thru panic.
|
|
*err = r.(Err)
|
|
f()
|
|
default:
|
|
panic(r)
|
|
}
|
|
}
|
|
|
|
// Catch is a convenient helper to those functions that doesn't return errors.
|
|
// Go's main function is a good example. Note! There can be only one deferred
|
|
// Catch function per non error returning function. See Handle for more
|
|
// information.
|
|
func Catch(f func(err Err)) {
|
|
if r := recover(); r != nil {
|
|
e, ok := r.(Err)
|
|
if !ok {
|
|
panic(r)
|
|
}
|
|
f(e)
|
|
}
|
|
}
|
|
|
|
// CatchAll is a helper function to catch and write handlers for all errors and
|
|
// all panics thrown in the current go routine.
|
|
func CatchAll(errorHandler func(err Err), panicHandler func(v interface{})) {
|
|
if r := recover(); r != nil {
|
|
e, ok := r.(Err)
|
|
if ok {
|
|
errorHandler(e)
|
|
} else {
|
|
panicHandler(r)
|
|
}
|
|
}
|
|
}
|
|
|
|
// CatchTrace catches all errors and prints their call stack.
|
|
// Setting the exit parameter to a value above 0 will make the program
|
|
// exit in case of an error.
|
|
func CatchTrace(exit int) {
|
|
if r := recover(); r != nil {
|
|
e, ok := r.(Err)
|
|
if !ok {
|
|
panic(r)
|
|
}
|
|
e.PrintCallStack()
|
|
if exit > 0 {
|
|
os.Exit(exit)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return is same as Handle but it's for functions which don't wrap or annotate
|
|
// their errors. If you want to annotate errors see Annotate for more
|
|
// information.
|
|
func Return(err *Err) {
|
|
if r := recover(); r != nil {
|
|
e, ok := r.(Err)
|
|
if !ok {
|
|
panic(r) // Not ours, carry on panicking
|
|
}
|
|
*err = e
|
|
}
|
|
}
|
|
|
|
// ReturnStd is like Return, but it returns the Go standard error type.
|
|
func ReturnStd(err *error) {
|
|
if r := recover(); r != nil {
|
|
e, ok := r.(Err)
|
|
if !ok {
|
|
panic(r) // Not ours, carry on panicking
|
|
}
|
|
*err = e.Unwrap()
|
|
}
|
|
}
|
|
|
|
// Annotate adds additional messages to the error.
|
|
func Annotate(err *Err, msg string) {
|
|
if r := recover(); r != nil {
|
|
e, ok := r.(Err)
|
|
if !ok {
|
|
panic(r) // Not ours, carry on panicking
|
|
}
|
|
*err = e
|
|
e.Annotate(msg)
|
|
} else if *err != nil { // if other handlers call recovery() we still..
|
|
(*err).Annotate(msg)
|
|
}
|
|
}
|