-
Notifications
You must be signed in to change notification settings - Fork 18.5k
Description
This proposal is for adding a new Causer interface for errors to the standard library to address the problem of error propagation through multiple packages. (This idea is inspired by the causer interface in the https://github.com/pkg/errors package.)
Consider three packages A, B, and C, where package A calls a function in package B and that function calls a function in package C. The general problem that this proposal addresses is how should package B propagate an error from package C back to package A.
There are three ways B could handle an error returned from C:
- B could return the error directly.
- B could return its own error, which exposes the error from package C by:
a. extracting some information from the error (e.g.fmt.Errorf("blah blah: %s", err))
b. embedding the original error (e.g. errors.Wrap).
Approach 1 neglects to add context, which could make it hard for package A to interpret the error. Approach 2a includes context from package B, but partially destroys information from the original error. Approach 2b is the most versatile, allowing package B to provide context while preserving the original error.
The problem with approach 2a currently is that there is no standard way to embed an error, and this is really something that benefits from a blessed interface that the Go ecosystem can then agree to use.
The interface is simple:
type Causer {
Cause() error
}
Example usage:
// Wrap returns a Causer with Cause set to the given error.
func Wrap(err error, msg string) error {}
// Cause recursively tests for Causer and calls Cause and returns the
// original error.
func Cause(e error) error {}
func Frob() error {
err := RetryTemporaryErrors(doRequest())
if err != nil {
orig := Cause(err)
if orig, ok := orig.(Timeout); ok && orig.Timeout() {
fmt.Println("Server is down")
} else {
fmt.Printf("Something weird happened: %s\n", orig)
}
}
return nil
}
func RetryTemporaryErrors(f func() error) error {
for {
err := f()
if err == nil {
return nil
}
if err, ok := err.(Temporary); !(ok && err.Temporary()) {
return Wrap(err, "not temporary error")
}
err = fiddleThing()
if err != nil {
return err
}
time.Sleep(10 * time.Second)
}
}
When I was almost done writing this, I found #25675 which appears to be suggesting something similar. However, this proposal is drastically narrower in scope (just about the Causer interface) and the rationale is that this really needs to be standardized to be useful since it is a problem with how errors cross package boundaries.