Search code examples
goerror-handling

How to properly use cockroachdb/errors


I am using Go and stumbled over cockroachdb/errors for error handling. I understand that errors.Wrap can be used to add more context and stack traces to errors, like this:

func DoSomething() error {
    err := os.WriteFile(...)
    if err != nil {
        return errors.Wrap(err, "something happened here in DoSomething")
    }
}

func CallSomething() error {
    err := DoSomething()
    if err != nil {
        return errors.WithDetail(err, "here is some additional info")
    }
}

Assuming, that the usage above is correct, I am not sure how to handle errors from third-party libraries. Consider the following:

func DoSomething() error {
    err := some.ThirdPartyLibraryCode(...)
    if err != nil {
        return errors.Wrap(err, "something happened here in DoSomething")
    }
}

If the third-party library also uses cockroachdb/errors, wrapping their errors with errors.Wrap might result in nested errors with multiple stack traces, which could produce confusing output:

(1) attached stack trace
  -- stack trace:
  | graph-editor/nodes.(*Graph).ExecuteImpl
  |     /Users/daniel/git/graph-main/[email protected]:52
  | [...repeated from below...]  👈👈👈👈 ??
Wraps: (2) failed to execute  

How should I handle errors from third-party libraries that might already be using cockroachdb/errors (or not) to avoid duplicated stack traces and keep the error output clear?


Solution

  • According to the documentation, creating an error with the New() family offers this options of printing related information:

    // - message via `Error()` and formatting using `%v`/`%s`/`%q`.
    // - everything when formatting with `%+v`.
    // - stack trace and message via `errors.GetSafeDetails()`.
    // - stack trace and message in Sentry reports.
    

    You're not telling us how you're printing the infos you find confusing. After playing around a bit with the library, I guess, you're using the + prefix for the v verb, e.g. with

    fmt.Printf("%+v", myCockroachDBErr)
    

    (Honestly, I don't know why it results in an output like the one you're posting, since according to the fmt documentation this just "adds field names". Go magixx.)

    So, with %+v you're opting for "everything" output, which is probabably rarely what you want (apart from debug sessions). You will see more or less all information the error struct holds, in a more or less comprehensable way. If you just want the straight-forward stack being printed, use

    fmt.Printf("%v", errors.GetSafeDetails(myCockroachDBErr))
    

    That said, it seems like Wrap() functions behave rather similar to the New() functions. That means, Wrap() creates a new error, and stores the wrapped one for further reference. While - according to the Wrap() docs - it "retains" the old error'sstack, it's apparently not considered part of the the new error's stack, which starts right where Wrap() is invoked. If you want to ouput the stack of the wrapped error, you have to unwrap it first:

    import (
        "fmt"
        "github.com/cockroachdb/errors"
    )
    
    func main() {
        wrappingErr := someFkt1()
        fmt.Printf("wrappingErr: %v\n", errors.GetSafeDetails(wrappingErr))
        unwrappedErr := errors.UnwrapOnce(errors.UnwrapOnce(wrappingErr)
        fmt.Printf("got2: %v\n", errors.GetSafeDetails(unwrappedErr))
        return
    }
    
    
    func someFkt1() error {
        return someFkt2()
    }
    func someFkt2() error {
        return errors.Wrapf(someFkt3(), "outer error")
    }
    
    func someFkt3() error {
        return errors.New("inner error")
    }
    

    For a better understanding, you can also output "everything" again:

    fmt.Printf("everything: %+v\n\n", err)
    

    Which will log smth like:

    everything: inner error
    (1) attached stack trace
      -- stack trace:
      | main.someFkt2
      |     /myPath/main.go:31
      | [...repeated from below...] 👈 i.e. /myPath/main.go:28, /myPath/main.go:11, proc.go:267, asm_amd64.s:1650
    Wraps: (2) attached stack trace 👈 the one we unwrap in my example
      -- stack trace:
      | main.someFkt3
      |     /myPath/main.go:35
      | main.someFkt2
      |     /myPath/main.go:31
      | main.someFkt1
      |     /myPath/main.go:28
      | main.cockroachDBErrors
      |     /myPath/main.go:11
      | runtime.main
      |     /usr/local/go/src/runtime/proc.go:267
      | runtime.goexit
      |     /usr/local/go/src/runtime/asm_amd64.s:1650
    Wraps: (3) inner error 👈 looks like even New() counts as wrapping
    Error types: (1) *withstack.withStack (2) *withstack.withStack (3) *errutil.leafError 👈 a helpful legend
    

    I figure you could create a loop unwrapping errors just before you've reached the inmost one, and output that error's stack trace to see it all from the start. I don't see a more straightforward way, though - errors.UnwrapAll() returns the the leafError, which doesn't bear a trace itself.