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

Go: Improve error handling #112

Closed
omarkohl opened this issue Jan 1, 2023 · 5 comments
Closed

Go: Improve error handling #112

omarkohl opened this issue Jan 1, 2023 · 5 comments
Milestone

Comments

@omarkohl
Copy link
Contributor

omarkohl commented Jan 1, 2023

Right now I'm wrapping with fmt.Errorf("%w") and using go-multierror to convert multiple errors into one.

Dave Cheney says he no longer wraps. Why? What is the alternative? github.com/pkg/errors is archived. https://groups.google.com/g/golang-nuts/c/FjtjR87-1qM

https://dr-knz.net/cockroachdb-errors-everyday.html
https://github.com/cockroachdb/errors
cockroach package seems a little bloated. Many others don't seem to be tracking the stack trace al all?

https://www.southcla.ws/go-error-wrapping/
Looks nice: https://github.com/Southclaws/fault
Has stack, has user friendly messages, but seems a rather new project, is it worth adding as a dependency?

Ideally what I would like:

  • Easy debugging when errors occur (stack trace or similar)
  • Nice (non-technical) error messages for end users
  • Not having to rip out the entire guts when some third-party dependency dies. Error handling is everywhere.
  • Support for multi errors (e.g. when validating user input that contains multiple errors). In all fairness, this is an exceptional case, not the rule. Go 1.20 added some support for this, but the corresponding libraries still need to implement it. The main downside of the stdlib errors.Join() function is that the errors are separated with a newline when calling Error(), which breaks usual formatting and logging.
@omarkohl omarkohl added this to Ideas Jun 8, 2023
@omarkohl omarkohl moved this to Draft in Ideas Jun 8, 2023
@omarkohl
Copy link
Contributor Author

@omarkohl omarkohl changed the title Go: Improve error handling [Idea] Go: Improve error handling Jun 18, 2023
@omarkohl
Copy link
Contributor Author

omarkohl commented Jul 29, 2023

https://github.com/rotisserie/eris

@omarkohl
Copy link
Contributor Author

omarkohl commented Jul 30, 2023

The most promising alternative right now is indeed https://github.com/cockroachdb/errors .

@omarkohl
Copy link
Contributor Author

https://github.com/Southclaws/fault

  • ✔️ Allows including information for end users
  • 🟡 Multi-error support is being considered Play nicely with multierror libraries Southclaws/fault#9
  • 🟡 Instead of entire stack traces it includes information about the line where the error was created
  • 🟡 Only a year old (June 2022), essentially single maintainer. Can be good because less baggage, but project is also more likely to disappear.
  • ❌ Contains more stuff (context, tags) than I need, but I don't need to use it.

@omarkohl
Copy link
Contributor Author

omarkohl commented Jul 30, 2023

Writing our own error lib.

  • ✔️ Only include what is really needed. Independence.
  • ❌ Re-invent the wheel.
  • ❌ Maintenance effort.

Some code I played around with. Note that there are a few open questions e.g. is a public Tree interface needed? Should the user friendly errors be part of the default type or a new optional type? Is having the simple location (source code line) of the error better than a full stack trace (taken from fault library)? Should the public functions return a type Tree or better error even if more internal casting would be required? Should recursion be replaced with loops?

type Tree interface {
	Error() string
	FriendlyError() string
	Unwrap() []error
	Location() string
}

func New(msg string) Tree {
	return new(msg, "")
}

func new(msg, friendlyMsg string) Tree {
	return defaultTree{
		msg:         msg,
		friendlyMsg: friendlyMsg,
		location:    getLocation(4),
		children:    nil,
	}
}

func NewFriendly(msg string, friendlyMsg string) Tree {
	return new(new, friendlyMsg)
}

// Newf includes formatting specifiers.
func Newf(message string, va ...any) Tree {
	return new(fmt.Sprintf(message, va...), "")
}

func Wrap(err error, msg string) Tree {
	return wrapMulti([]error{err}, msg)
}

func WrapMulti(errs []error, msg string) Tree {
	return wrapMulti(errs, msg)
}

func wrapMulti(errs []error, msg string) Tree {
	allNil := true
	for _, e := range errs {
		if e != nil {
			allNil = false
		}
	}
	if len(errs) == 0 || allNil {
		return nil
	}
	var children []Tree
	for _, e := range errs {
		if child, ok := e.(Tree); ok {
			children = append(children, child)
		} else {
			children = append(children, externalError{err: e})
		}
	}
	return defaultTree{
		msg:      msg,
		location: getLocation(4),
		children: children,
	}
}

type defaultTree struct {
	msg         string
	friendlyMsg string
	location    string
	children    []Tree
}

func (t defaultTree) Error() string {
	switch len(t.children) {
	case 0:
		return t.msg
	case 1:
		return fmt.Sprintf("%v: %v", t.msg, t.children[0].Error())
	default:
		var cMsgs []string
		for _, c := range t.children {
			cMsgs = append(cMsgs, c.Error())
		}
		return fmt.Sprintf("%v: [%v]", t.msg, strings.Join(cMsgs, " | "))
	}
}

func (t defaultTree) FriendlyError() string {
	switch len(t.children) {
	case 0:
		return t.msg
	case 1:
		return fmt.Sprintf("%v %v", t.msg, t.children[0].Error())
	default:
		var cMsgs []string
		for _, c := range t.children {
			cMsgs = append(cMsgs, c.Error())
		}
		return fmt.Sprintf("%v (%v)", t.msg, strings.Join(cMsgs, " ; "))
	}
}

func (t defaultTree) Unwrap() []error {
	var errs []error
	for _, c := range t.children {
		errs = append(errs, c)
	}
	return errs
}

func (t defaultTree) Location() string {
	return t.location
}

// externalError wraps errors not created by 'errtree'.
// It adds no information and acts as a transparent gateway.
type externalError struct {
	err error
}

func (t externalError) Error() string {
	return t.err.Error()
}

func (t externalError) FriendlyError() string {
	return ""
}

func (t externalError) Unwrap() []error {
	return []error{t.err}
}

func (t externalError) Location() string {
	return ""
}

func getLocation(skip int) string {
	pc := make([]uintptr, 1)
	runtime.Callers(skip, pc)
	cf := runtime.CallersFrames(pc)
	f, _ := cf.Next()

	return fmt.Sprintf("%s:%d", f.File, f.Line)
}

@omarkohl omarkohl removed this from Ideas Aug 4, 2023
@omarkohl omarkohl moved this from Backlog to In Progress in Implementation Aug 4, 2023
@omarkohl omarkohl changed the title [Idea] Go: Improve error handling Go: Improve error handling Aug 4, 2023
@omarkohl omarkohl added this to the v0.4.0 milestone Aug 4, 2023
omarkohl added a commit that referenced this issue Aug 4, 2023
cockroachdb/errors gives us stack traces and information for end users
with WithHint().

The reason for the 'errors' wrapper package is so as not to spread a
third party dependency all over the code and make it easier to replace
should that ever become necessary. If the API of the internal 'errors'
keeps growing and becoming closer and closer to cockroachdb/errors then
at some point the internal package might be deleted.

In schema.resolvers.go it was necessary to import the packages as
'errors2' because otherwise the automated code generation kept
overriding it.
@omarkohl omarkohl closed this as completed Aug 4, 2023
@github-project-automation github-project-automation bot moved this from In Progress to Done in Implementation Aug 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

No branches or pull requests

1 participant