Go wrapped errors

I wanted a way to return an error value without losing any deeper error information. For example in my database code, I have an error I return when a get operation has failed e.g. ErrDatabaseGetFailed. This is independent of the store (bolt/inmem/etc) but I still want to return why it failed specifically, e.g. something went wrong with boltdb/inmem etc, whatever.

I wanted a way to do something like

if err == MyErr

and

if err == SpecificErr

or/and get further information to log. However, go only has the fmt error wrap, I can’t return an existing error value I’ve defined AND the deeper one in the same error so I came up with this.

type wrappedError struct {
	innerErr error
	outerErr error
}

func (e *wrappedError) Error() string { return e.outerErr.Error() }

func (e *wrappedError) Is(v error) bool { return v == e.outerErr }

func (e *wrappedError) Unwrap() error { return e.innerErr }

func (e *wrappedError) As(target interface{}) bool {
	return errors.As(e.outerErr, target)
}

func Wrap(outerErr error, innerErr error) error {
	return &wrappedError{outerErr: outerErr, innerErr: innerErr}
}

This allows me to return a hierarchy of errors, both custom types and error values simply by

return Wrap(ErrDatabaseGetFailed, err)

the wrapped error is seen as ErrDatabaseGetFailed. You can’t compare it with ‘==’ but that’s probably not a good idea anyway, although I’ll admit I’ve been doing that until recently.

instead

errors.Is(err, ErrDatabaseGetFailed)
errors.Is(err, ErrFromBoltDB)

Why doesn’t go’s built-in error package have something like this?

1 Like

I think it similar to error wrapping: Working with Errors in Go 1.13 - go.dev

2 Likes

Perhaps there’s not one agreed-upon good way to do this yet, so there isn’t a standard way built into the Go standard library.

For example…

I learned Go after I learned Python and C#, so I wrote my own errors package that had Python’s concept of a __cause__ and a __context__, so my error type was essentially:

type myError struct {
    err error
    cause error
    context error
}

Where err and cause behave similarly to your outerErr and innerErr, respectively, but there was an additional context field for any errors encountered while attempting to handle an error.

Later on, I wrote another errors subpackage for another project and that does away with the cause vs. context distinction and instead has an []error slice whose first error is the outermost error and the last is the innermost.

Some pros to this implementation are that:

  • It is easier for me to represent errors from functions that fan-out and fan-in results from multiple concurrent goroutines.
  • When iterating through deeply nested errors, it has better performance characteristics than iterating through 2-error field structs which behave like linked lists or like trees when iterating through them with errors.Is/errors.As.

However, one of the major cons to this is that wrapping errors essentially does:

func wrap(outer error, inner error) error {
    switch inner := inner.(type) {
    // ...
    case myCustomErrorSlice:
        newErrors := make(myCustomErrorSlice, len(inner) + 1)
        copy(newErrors[1:], inner)
        newErrors[0] = outer
        return newErrors
    // ...
    }
}

Which can be slow to allocate and copy, and result in a lot of memory usage if something is keeping those inner error slices alive.

Tl;dr:

My point is that there are many ways to generate and propagate errors to callers and that they have trade-offs that may require different users to write their own error types that solve the problem differently. errors.As and errors.Is makes it possible to use these different error types together.

1 Like

In their example:

return fmt.Errorf("access denied: %w", ErrPermission)

Would only allow me to annotate an existing error with additional information. I wanted to return an error from within an error to indicate where the error came from. For example, my database code is generic, I persist a type that contains a byte slice of data, on a higher level each repository type uses this code. By returning an error within an error I can trace exactly where that error came from but I could also have custom errors implement a Message() method to provide me with log information of what inputs could have caused that error in the first place. I could do this in my logging middleware and just tag that on as it returns.