fmt.Println() stops printing chain at wrapped custom error (golang)

Viewed 218

i want to know why fmt.Println() in Go/golang does print all the errors of some error-chain, when all wrapped errors in that error-chain where created with fmt.Errorf(). But when one of these errors is a custom error (that also wraps), fmt.Println() stops unwrapping and printing of error chain, at the custom error position.

Here is some simplified sample code:

type CustomError struct {
    Msg string
    Err error
}

func (e *CustomError) Error() string {
    return e.Msg
}

func (e *CustomError) Unwrap() error {
    return e.Err
}

func main() {
    level1Err := errors.New("[Error in L1]: Boom")
    level2Err := fmt.Errorf("[Error in L2]: Wrap L1Err %w", level1Err)
    level3Err := fmt.Errorf("[Error in L3]: Wrap L2Err %w", level2Err)
    //level3Err := &CustomError{"[Error in L3]: Wrap L2Err", level2Err}
    level4Err := fmt.Errorf("[Error in L4]: Wrap L3Err %w", level3Err)
    fmt.Println(level4Err)
}

// Console output, when uncomment line28 and comment line29:
// [Error in L4]: Wrap L3Err [Error in L3]: Wrap L2Err [Error in L2]: Wrap L1Err [Error in L1]: Boom

// Console output, when uncomment line29 and comment line28:
// [Error in L4]: Wrap L3Err [Error in L3]: Wrap L2Err

Using Go 1.18

You can also found the sample code at https://github.com/MBODM/golang-error-chains-problem

Any help is rather appreciated

2 Answers

If you look at an existing example from go source code:

// SyscallError records an error from a specific system call.
type SyscallError struct {
    Syscall string
    Err     error
}

func (e *SyscallError) Error() string { return e.Syscall + ": " + e.Err.Error() }

func (e *SyscallError) Unwrap() error { return e.Err }

So your CustomError#Error() method should be:

func (e *CustomError) Error() string {
    if e.Err == nil {
        return e.Msg
    }
    return e.Msg + ": " + e.Err.Error()
}

See this playground example:

[Error in L4]: Wrap L3Err [ErrorC in L3]: Wrap L2Err: [Error in L2]: Wrap L1Err [Error in L1]: Boom

To help other ppls like me (i had a hard time with this, as a Go newbie), i answer my own question here:

It seems fmt.Println(topLevelErr) NOT unwraps all the errors and prints all that errors (by calling .Error() of every unwrapped error) with some ": " as delimiter between the errors, as i thought initially. This is NOT what happens.

Instead all the go-lib errors do this by themselfes, by implementing the error interface in this way: Their Error() method (error implementation) tests if they have some wrapped error inside of themselfes and if so, print it "chained". This means they print their own error msg + ": " + the wrapped error´s msg.

You can see this, if you have a look at the Go source code, as VonC mentioned below.

I personally would call this a rather stupid design. And totally unnecessary too, since all errors are wrapped anyway. Therefore all errors could be unwrapped manually, via errors.Unwrap() method from go-lib. When unwrapped, we (or also fmt.Println()) could print the error msg of every error, together with some delimiting ": " stuff. This would be a way better design, than "every error is wrapping the msg of it´s inner error inside it´s own msg". I think that´s the reason why it confused me a lot. :)

Also i advice you to do the same thing as i did in sample code below: At least let `fmt.Errorf()` create that wrapped error msg, instead of putting some `": "` string into it, by yourself. So it is granted, when the wrapped error behaviour changes, yours do also.

Previous advice was incorrect, so i removed it. I accidentally thought fmt.Errorf() adds a ":" to the text, when wrapping the error with %w. This is not true. As VonC already pointed out: Most stuff in the Go standard library do this in the implementation of their error interface:

return "own error text" + ": " + e.err.Error()

So it doesnt matter, which of these you use, in your error implementation:

  • return fmt.Errorf("%s: %w", "own error text", e.err).Error()
  • return "own error text" + ": " + e.err.Error()

In both cases, you need to do the ": " thingy by your own. In short: The fmt.Errorf() function does NOT automatically put the ": " into the created error´s msg.

This shows, what´s all about:

type CustomError struct {
    Msg string
    Err error
}

func (e *CustomError) Error() string {
    if e.Err != nil {
        // Added a ":" here, because fmt.Errorf() does NOT
        // automatically do this (as i accidentally thought above)!
        wrappedError := fmt.Errorf("%s: %w", e.Msg, e.Err)
        wrappedErrorMsg := wrappedError.Error()
        return wrappedErrorMsg
        // Also you can do this instead (like most go-lib pkgs):
        // return e.Msg + ": " + e.Err.Error()
    }
    return e.Msg
}

func (e *CustomError) Unwrap() error {
    return e.Err
}

func printAllWrappedErrors(topLevelError error) {
    fmt.Println(topLevelError)
}

func printCustomErrorOnly(topLevelError error) {
    var e *CustomError
    if errors.As(topLevelError, &e) {
        fmt.Println(e.Msg) // <-- This is the difference
    }
}

func printCustomErrorIncludingAllWrappedErrors(topLevelError error) {
    var e *CustomError
    if errors.As(topLevelError, &e) {
        fmt.Println(e) // <-- This is the difference
    }
}

For a better understanding just play around with the sample. You can find it here on GitHub.

And big THX to VonC for the prompt help and the Kickoff! Was very helpful! :)

Have fun.

Related