bracesdev / errtrace

An alternative to stack traces for your Go errors
https://pkg.go.dev/braces.dev/errtrace
BSD 3-Clause "New" or "Revised" License
721 stars 10 forks source link

Expose traceTree or some variant #71

Closed bufdev closed 4 months ago

bufdev commented 9 months ago

This was motivated by wanting to get a printout of just the stack trace, without the underlying error message. That use was was a situation where I had error interceptor logic in place, but didn't control the underlying location where the error was printed, as that was controlled by a different library (github.com/spf13/cobra). I just wanted to get a stack trace to print out to a debug logger, but realized I couldn't just yet.

This could take different forms:

Potentially the most idiomatic way to do this would be to expose some custom error type where I can use errors.Is/As, which would solve the problem more generally, but I assume this was ruled out.

I may be missing something here, just starting to muck around with the library - apologies if this is polluting the issue space!

abhinav commented 9 months ago

Hey, @bufdev!

I think exporting traceTree is okay: it doesn't have any internal details—not even the program counters. It's just a plain data structure. (Thinking out loud, probably: func Inspect(error) *Tree.)

Although I agree—exporting this is not my favorite solution. However, it seems better than an overly complicated error tree walker, which is the other option we were considering here to optimize on allocs during formatting. We might still do that in the future, but it could be an internal detail for formatting and the public Inspect function can remain.

@prashantv Do you have thoughts on this?

bufdev commented 9 months ago

Oh, that'd be great then if possible :-) But no worries either way. One suggestion: If you do expose Tree directly, some type of iterator-type function to iterate over what are now the traceFrames for the both Trace and Children would be fantastic, but I won't get picky.

abhinav commented 9 months ago

@bufdev Just confirming one thing for what you were trying to do: you didn't need to know/track the parent/child relationships between errors, right? You just wanted errors and their traces?

bufdev commented 9 months ago

Yea I'm not interested in the relationships between the errors.

prashantv commented 9 months ago

Since we allow annotating an error with a specific frame, I wonder if we should expose the converse -- an API that extracts a single frame from an error (if it's an errtrace wrapped error). The caller can then build up the same tree by unwrapping.

I'm OK with also having more APIs to expose the tree, though I'd start with the lowest-level first, then the more common need (customization options for formatting).

abhinav commented 9 months ago

I like starting with that. Strawman:

// UnwrapFrame unwraps the errtrace frame
// stored inside the given error.
//
// inner is the error inside the errtrace, if any.
// ok reports whether a frame was available.
func UnwrapFrame(error) (inner error, frame Frame, ok bool)

(I want to not take the name "unwrap" in case we want to provide an errors.Unwrap analog per https://github.com/bracesdev/errtrace/issues/38#issuecomment-1828941228.)

So a user could do something like:

// func printTrace(w io.Writer, err error) {

err, frame, ok := UnwrapFrame(err)
for ok {
  printFrame(w, frame)
  err, frame, ok = UnwrapFrame(err)
}

if err == nil {
  // end of stack
  return
}

if multi, ok := err.(interface{ Unwrap() []error }); ok {
  for _, err := range multi.Unwrap() {
    fmt.Fprintln(w) // separate traces with newlines
    printTrace(w, err)
  }
}