errors

package module
v2.0.1 Latest Latest
Warning

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

Go to latest
Published: Jan 13, 2021 License: BSD-2-Clause Imports: 5 Imported by: 0

README

Quality Gate Status

Docs

See https://godoc.org/github.com/pashaosipyants/errors

License

BSD-2-Clause, see LICENSE file.

An initial point of this project was forking https://github.com/pkg/errors. It's license is in LICENSE - fork file. (BSD-2-Clause as well)

Documentation

Overview

Package errors follows 2 goals:

1. Provide a simple way to enrich an error with some context details, using go13 Wrap/Unwrap mechanism. Such types of context are: - stacktrace - annotation - additional msg - error, which is suppressed by newer one - any value that can be accessed by corresponding key any time later

2. Provide a convenient way to handle errors in exception style.

About first point:

Error might look like a context vice versa. It also flows through the function call stack, but, opposite to context, it flows upwards. While it flows we want to enrich it with some information relevant to current stack level. This package provides several wrapping constructs providing this ability.

One can wrap an existing error with an error with a stacktrace, so that this error will be possible to print with it's stacktrace and easily find a place error occured in. Only the most deep stacktrace is saved and printed.

One can add an annotation to an error. Moreover, if error already has a stacktrace, this annotation will be printed along with the corresponding line in stacktrace, so it's easy to find in which function this annotations was added.

One can suppress an existing error with newer one. Both errors will be printed.

One can add a value to an error. Later this value can be retrieved with the key used to add this value. If several values are added with the same key, only the first one (from the most deep stack level) is saved. It might be very useful to pass logger to the function upper in the stack. Look at the example to see this approach.

About second point:

Go's error handling idiom usually looks like

x, err := DoSmth()
if err != nil {
    logger.Error(err)
    // may be some err processing
    return err
}

It is very "holy war" question which way to handle errors, exception like or "returning err" like, is better. Everyone chooses himself which way is more appropriate to him. But doubtlessly "returning err" style has some disadvantages.

1. Error handling logic is mixed with "business" logic so it shadows it

2. Code repetitions, which can cause a bug

3. Verbosity

That's why Go 2 draft https://go.googlesource.com/proposal/+/master/design/go2draft.md contains new Handle-Check error handling approach, which is quite similar to exception approach. This package provides similar to this Handle-Check mechanism, based on panics. Details are below =)

Example of error format, when printed with SprintE:

ERROR:
task can not be marked as done

STACK:
github.com/pashaosipyants/errors/v2_test.markTaskAsDone
	/work/go/src/github.com/pashaosipyants/errors/example_compehensive_test.go:105
github.com/pashaosipyants/errors/v2_test.apiCreateTask.func1
	/work/go/src/github.com/pashaosipyants/errors/example_compehensive_test.go:78
github.com/pashaosipyants/errors/v2.Handler
	/work/go/src/github.com/pashaosipyants/errors/handlecheck.go:55
github.com/pashaosipyants/errors/v2_test.apiCreateTask
	/work/go/src/github.com/pashaosipyants/errors/example_compehensive_test.go:90
github.com/pashaosipyants/errors/v2_test.Example.func1
	work/go/src/github.com/pashaosipyants/errors/example_compehensive_test.go:60
github.com/pashaosipyants/errors/v2_test.Example
	/work/go/src/github.com/pashaosipyants/errors/example_compehensive_test.go:63
testing.runExample
	/Go/src/testing/run_example.go:62
testing.runExamples
	/Go/src/testing/example.go:44
testing.(*M).Run
	/Go/src/testing/testing.go:1200
main.main
	_testmain.go:58
runtime.main
	/Go/src/runtime/proc.go:203
runtime.goexit
	/Go/src/runtime/asm_amd64.s:1373

SUPPRESSED:
ERROR:
User: 239; Error: already_exist_but_not_done

STACK:
github.com/pashaosipyants/errors/v2/example_auxiliary.SaveTaskToDbMockExistButNotDone
	/work/go/src/github.com/pashaosipyants/errors/example_auxiliary/example_auxiliary.go:36
github.com/pashaosipyants/errors/v2/example_auxiliary.CreateTaskInitedByUser
	/work/go/src/github.com/pashaosipyants/errors/example_auxiliary/example_auxiliary.go:78
	ANNOTATION: Inited by user 239
github.com/pashaosipyants/errors/v2_test.apiCreateTask
	/work/go/src/github.com/pashaosipyants/errors/example_compehensive_test.go:89
github.com/pashaosipyants/errors/v2_test.Example.func1
	/work/go/src/github.com/pashaosipyants/errors/example_compehensive_test.go:60
github.com/pashaosipyants/errors/v2_test.Example
	/work/go/src/github.com/pashaosipyants/errors/example_compehensive_test.go:63
testing.runExample
	/Go/src/testing/run_example.go:62
testing.runExamples
	/Go/src/testing/example.go:44
testing.(*M).Run
	/Go/src/testing/testing.go:1200
main.main
	_testmain.go:58
runtime.main
	/Go/src/runtime/proc.go:203
runtime.goexit
	/Go/src/runtime/asm_amd64.s:1373
Example

This is comprehensive, pretending to be close to real-life example of using this package. It's easier to see it in code, but if you use godoc, please, notice https://godoc.org/github.com/pashaosipyants/errors/example_auxiliary package, which is used here.

Imagine there is an api to create a task that is executed by another service. Besides user of this api wants to hold info whether this task is already done. This api, ofc, can return error. E.g. certain task may be already created. If so, error should report whether it is done or not.

package main

import (
	"bytes"
	"fmt"
	"os"

	"github.com/sirupsen/logrus"

	. "github.com/pashaosipyants/errors/v2"
	"github.com/pashaosipyants/errors/v2/example_auxiliary"
)

// This is comprehensive, pretending to be close to real-life example of using this package.
// It's easier to see it in code, but if you use godoc, please, notice
// https://godoc.org/github.com/pashaosipyants/errors/example_auxiliary package,
// which is used here.
//
// Imagine there is an api to create a task that is executed by another service.
// Besides user of this api wants to hold info whether this task is already done.
// This api, ofc, can return error. E.g. certain task may be already created.
// If so, error should report whether it is done or not.
func main() {
	l := logrus.New().WithField("application", "tasks")
	l.Logger.SetFormatter(TerminalVerbose{})
	l.Logger.SetOutput(os.Stdout)

	// loop to work out different cases
	for i := 0; i < 4; i++ {
		// func is smth like try block here
		func() {
			defer l.Infof("\n\tCase %d finished\n-------------------------------------------\n\n\n", i)

			// smth like catch block
			defer Handler(func(err error) {
				switch ValueE(err, "api") {
				case errcode_apicreatetaskfailed:
					logger, ok := ValueE(err, "logger").(*logrus.Entry) // logger with relevant fields of functions deeper in the call stack
					if !ok {
						logger = l
					}
					var ue *UserError
					if AsE(err, &ue) {
						logger = logger.WithField("user", ue.user)
					}
					logger.Error(SprintE(err)) // log
					// may be some specific actions
				case errcode_apiuserloginfailed:
					// may be some specific actions
					panic("Assertion failed") // but in our example can't be here
				default:
					panic("Assertion failed")
				}
			})

			Check(
				apiUserLogin(l), OValue("api", errcode_apiuserloginfailed))

			Check(
				apiCreateTask(l, i), OValue("api", errcode_apicreatetaskfailed))

			l.Info("Success!!!\n") // log
		}()
	}

}

const errcode_apicreatetaskfailed = "api_create_task_failed"
const errcode_apiuserloginfailed = "api_user_login_failed"

func apiCreateTask(l *logrus.Entry, i int) (reterr error) {
	defer Handler(func(err error) {
		// do some specific logic - e.g. mark task in db as done
		switch {
		case IsE(err, example_auxiliary.ErrTaskAlreadyExistButNotDone):
			errOnMark := markTaskAsDone()
			reterr = AnyE(
				WrapE(errOnMark, OSupp(err)),
				err,
			)
		default:
			reterr = err
		}
		// common logic
	})

	err := example_auxiliary.CreateTaskInitedByUser(l, i, 239)
	Check(WrapUserError(err, 239))

	return nil
}

// pretends to be always success
func apiUserLogin(l *logrus.Entry) error {
	// some work

	return nil
}

// pretends that there is an error an task can not be marked as done
func markTaskAsDone() (reterr error) {
	defer DefaultHandler(&reterr)
	CheckIf(true, Error("task can not be marked as done"))
	return
}

func WrapUserError(err error, user int) error {
	if err == nil {
		return nil
	}
	return &UserError{
		user: user,
		err:  err,
	}
}

type UserError struct {
	user int
	err  error
}

func (x *UserError) Error() string {
	return x.err.Error()
}

func (x *UserError) Unwrap() error {
	return x.err
}

func (x *UserError) Format(f fmt.State, verb rune) {
	if verb == 's' {
		fmt.Fprint(f, x.err.Error())
	} else {
		fmt.Fprintf(f, "User: %d; Error: %v", x.user, x.err)
	}
}

///////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////

type TerminalVerbose struct {
}

const breaker = "........................................................\n"

func (t TerminalVerbose) Format(e *logrus.Entry) ([]byte, error) {
	if e.Level == logrus.InfoLevel {
		msg := lineBreaker(e.Message)
		return []byte(msg), nil
	}

	msg := lineBreaker(e.Message)

	var fields string
	for k, v := range e.Data {
		msg := fmt.Sprint(v)
		msg = lineBreaker(msg)
		fields += "--" + k + ":\n" + msg
	}

	var b *bytes.Buffer
	if e.Buffer != nil {
		b = e.Buffer
	} else {
		b = &bytes.Buffer{}
	}

	_, err := b.WriteString("\n" + breaker + msg + "\n" + fields)
	if err != nil {
		return nil, err
	}

	return b.Bytes(), nil
}

func lineBreaker(in string) string {
	if len(in) > 0 && in[len(in)-1] != '\n' {
		in += "\n"
	}
	return in
}
Output:

wrong output specially to make this function be executed and see output of this example

Index

Examples

Constants

View Source
const ErrStackMaxDepth = 32

Maximum depth of stack recorded to an error. One can change it, but be cautious. Set value only in initialization part of a program before any error is created not to cause concurrency problems.

Variables

View Source
var AsE = errors.As
View Source
var IsE = errors.Is
View Source
var NewE = errors.New
View Source
var UnwrapE = errors.Unwrap

Functions

func AnyE

func AnyE(errs ...error) error

AnyE is a helper function that returns first not nil error or nil if there are none.

func AnyFuncE

func AnyFuncE(errfuncs ...func() error) error

AnyFuncE is a helper function that executes funcs one by one, stops if error occurred and returns it

func Check

func Check(err error, opts ...OptionE)

Check panics with an error. One can handle this panic defering Handler func. opts allows to add context to the error. OStack is enabled by default.

func CheckIf

func CheckIf(ifErr bool, err error, opts ...OptionE)

CheckIf panics with an error if ifErr is true. One can handle this panic defering Handler func. opts allows to add context to the error. OStack is enabled by default.

func CheckIfNoStack

func CheckIfNoStack(ifErr bool, err error, opts ...OptionE)

The same as CheckIf, but OStack is not enabled by default.

func CheckNoStack

func CheckNoStack(err error, opts ...OptionE)

The same as Check, but OStack is not enabled by default.

func DefaultHandler

func DefaultHandler(reterr *error, elseDefer ...func())

as Handler, but instead of running your handle function put arisen error in reterr.

func Error

func Error(text string, opts ...OptionE) error

Creates new error. Use options to fill error's context.

func Errorf

func Errorf(format string, a ...interface{}) errf

Creates new error. E.g.: Errorf("user with id %s is absent", userID).Opts(OStack(), OAnno(text))

func Handler

func Handler(handle func(err error), elseDefer ...func())

defer Handler func and provide handler to process panics made by Check...(). Panics with type different to this package's one are just forwarded. elseDefer - deferred functions which will be executed if no error arisen. other panics are not intercepted. don't forget that Handler may not obtain error if u recover earlier.

func SprintE

func SprintE(err error) string

Returns detailed error description. With error msg, stacktrace, annotations, and suppressed errors.

func SuppressedE

func SuppressedE(err error) (supps []error)

func ValueE

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

Gets value by key saved in err. Returns nil if not found.

func WrapAnnotationE

func WrapAnnotationE(err error, annotation string, skip ...int) error

WrapAnnotationE returns error with err wrapped in and annotation(additional message) added. returnederr.Error() will be the same as err.Error(), but one can use SprintE(returnederr) to print it with annotations. If err is nil returns nil. UnwrapE(returnederr) == err. The sense of annotation is that by SprintE it's printed along with corresponding stacktrace level. So it's obvious in what place in code this annotation was added.

skip is optional param. First of variadic parameters is used, else are ignored. skip specifies the number of stacktrace levels to skip. By default stacktrace starts with the caller of WrapAnnotationE.

func WrapE

func WrapE(err error, opts ...OptionE) error

WrapE - one function that does anything other Wrap... functions can. Instead of doing smth like WrapStackE(WrapAnnotationE(WrapValueE(err, key, value), "text")) just use several options in one WrapE. Like this: WrapE(err, OStack(), OAnno(text), OValue(key, value)).

func WrapStackE

func WrapStackE(err error, skip ...int) error

WrapStackE returns error with err wrapped in and stacktrace recorded. returnederr.Error() will be the same as err.Error(), but one can use SprintE(returnederr) to print it with stacktrace. If err is nil returns nil. UnwrapE(returnederr) == err.

skip is optional param. First of variadic parameters is used, else are ignored. skip specifies the number of stacktrace levels to skip. By default stacktrace starts with the caller of WrapStackE.

func WrapSuppressedE

func WrapSuppressedE(err, suppressed error) error

WrapSuppressedE returns error with err wrapped in and suppressed error added. returnederr.Error() will be the same as err.Error(), but one can use SprintE(returnederr) to print it with suppressed error. If err is nil returns nil. UnwrapE(returnederr) == err. List of suppressed errors can be retrieved with the SuppressedE.

func WrapValueE

func WrapValueE(err error, key, value interface{}) error

WrapValueE returns error with err wrapped in and value added. returnederr.Error() will be the same as err.Error(). If err is nil returns nil. UnwrapE(returnederr) == err. Value can be retrieved from returnederr(or any wrappers of it) by specified key with the ValueE function.

Types

type OptionE

type OptionE func(error, int) error

func OAnno

func OAnno(annotation string) OptionE

func OStack

func OStack() OptionE

func OSupp

func OSupp(suppressed error) OptionE

func OValue

func OValue(key, value interface{}) OptionE

type ToSkipE

type ToSkipE int

func (ToSkipE) WrapE

func (skip ToSkipE) WrapE(err error, opts ...OptionE) error

The same as WrapE, but skip can be specified to skip several stacktrace levels. This is useful when WrapE is used inside some utility function which will be useless in stacktrace. E.g. see implementation of Error in this package. It utilizes WrapE, but skips itself from the stacktrace.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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