Documentation ¶
Overview ¶
Package errc simplifies error and defer handling.
Overview ¶
Package errc is a burner package: a proof-of-concept to explore better semantics for error and defer handling. Error handling and deferring using this package looks like:
func foo() (err error) { e := errc.Catch(&err) defer e.Handle() r, err := getReader() e.Must(err, msg("read failed")) e.Defer(r.Close)
Checking for a nil error is replaced by a call to Must on an error catcher. A defer statement is similarly replaced by a call to Defer.
The Problem ¶
Error handling in Go can also be tricky to get right. For instance, how to use defer may depend on the situation. For a Close method that frees up resources a simple use of defer suffices. For a CloseWithError method where a nil error indicates the successful completion of a transaction, however, it should be ensured that a nil error is not passed inadvertently, for instance when there is a panic if a server runs out of memory and is killed by a cluster manager.
For instance, a correct way to commit a file to Google Cloud Storage is:
func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) { client, err := storage.NewClient(ctx) if err != nil { return err } defer client.Close() w := client.Bucket(bucket).Object(dst).NewWriter(ctx) err = errPanicking defer func() { if err != nil { _ = w.CloseWithError(err) } else { err = w.Close() } } _, err = io.Copy(w, r) return err }
The err variable is initialized to errPanicking to ensure a non-nil err is passed to CloseWithError when a panic occurs. This ensures that a panic will not cause a corrupted file. If all went well, a separate path used to collect the error returned by Close. Returning the error from Close is important to signal retry logic the file was not successfully written. Once the Close of w is successful all further errors are irrelevant. The error of the first Close is therefor willfully ignored.
These are a lot of subtleties to get the error handling working properly!
The same can be achieved using errc as follows:
func writeToGS(ctx context.Context, bucket, dst, src string) (err error) { e := errc.Catch(&err) defer e.Handle() client, err := storage.NewClient(ctx) e.Must(err) e.Defer(client.Close, errd.Discard) w := client.Bucket(bucket).Object(dst).NewWriter(ctx) e.Defer(w.CloseWithError) _, err = io.Copy(w, r) return err }
Observe how a straightforward application of idiomatic check-and-defer pattern leads to the correct results. The error of the first Close is now ignored explicitly using the Discard error handler, making it clear that this is what the programmer intended.
Error Handlers ¶
Error handlers can be used to decorate errors, log them, or do anything else you usually do with errors.
Suppose we want to use github.com/pkg/errors to decorate errors. A simple handler can be defined as:
type msg string func (m msg) Handle(s errc.State, err error) error { return errors.WithMessage(err, string(m)) }
This handler can then be used as follows:
func writeToGS(ctx context.Context, bucket, dst, src string) (err error) { e := errc.Catch(&err) defer e.Handle() client, err := storage.NewClient(ctx) e.Must(err, msg("error opening client")) e.Defer(client.Close) w := client.Bucket(bucket).Object(dst).NewWriter(ctx) e.Defer(w.CloseWithError) _, err = io.Copy(w, r) e.Must(err, msg("error copying contents")) return nil }
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Catcher ¶
type Catcher struct {
// contains filtered or unexported fields
}
A Catcher coordinates error and defer handling.
func Catch ¶
Catch returns an error Catcher, which is used to funnel errors from panics and failed calls to Try. It must be passed the location of an error variable. The user must defer a call to Handle immediately after the creation of the Catcher as follows:
e := errc.Catch(&err) defer e.Handle()
Example ¶
package main import ( "io" "io/ioutil" "os" "strings" "github.com/mpvl/errc" ) func newReader() (io.ReadCloser, error) { return ioutil.NopCloser(strings.NewReader("Hello World!")), nil } func main() { func() (err error) { e := errc.Catch(&err) defer e.Handle() r, err := newReader() // contents: Hello World! e.Must(err) e.Defer(r.Close) _, err = io.Copy(os.Stdout, r) e.Must(err) return nil }() }
Output: Hello World!
Example (Pipe) ¶
package main import ( "io" "io/ioutil" "os" "strings" "github.com/mpvl/errc" ) func newReader() (io.ReadCloser, error) { return ioutil.NopCloser(strings.NewReader("Hello World!")), nil } func main() { r, w := io.Pipe() go func() { var err error e := errc.Catch(&err) defer e.Handle() e.Defer(w.CloseWithError) r, err := newReader() // contents: Hello World! e.Must(err) e.Defer(r.Close) _, err = io.Copy(w, r) }() io.Copy(os.Stdout, r) // The above goroutine is equivalent to: // // go func() { // // err is used to intercept downstream errors. Note that we set it to a // // sentinel even though we recover the panic below to cover the case of // // a panic between the two defers. This is very unlikely to be // // necessary, but remember: a panic may be caused by external factors // // and code requiring high reliability should always consider the // // possibility of a panic occurring at any point. // var err = errors.New("panicking") // // // No need to intercept error: io.PipeWriter.CloseWithError always // // returns nil. // defer w.CloseWithError(err) // // // Ensure that CloseWithError is not called with a nil error on panic. // // In this case use recover: because we set err multiple times, it // // seems a bit easier than managing everything by sentinel. // defer func() { // if v := recover(); v != nil { // err = errors.New("panicking") // } // }() // // r, err := newReader() // if err != nil { // return // } // defer func() { // if errC := r.Close(); err == nil { // err = errC // } // }() // // _, err = io.Copy(w, r) // }() }
Output: Hello World!
func (*Catcher) Defer ¶
Defer defers a call to x, which may be a function of the form:
- func()
- func() error
- func(error)
- func(error) error
- func(State) error
An error returned by any of these functions is passed to the error handlers.
Performance-sensitive applications should use DeferFunc.
Example (CancelHelper) ¶
ExampleCatcher_Defer_cancelHelper shows how a helper function may call a defer in the caller's E. Notice how contextWithTimeout taking care of the call to Defer is both evil and handy at the same time. Such a thing would likely not be allowed if this were a language feature.
package main import ( "context" "net/http" "time" "github.com/mpvl/errc" ) func do(ctx context.Context) { } func main() { contextWithTimeout := func(e *errc.Catcher, req *http.Request) context.Context { var cancel context.CancelFunc ctx := req.Context() timeout, err := time.ParseDuration(req.FormValue("timeout")) if err == nil { // The request has a timeout, so create a context that is // canceled automatically when the timeout expires. ctx, cancel = context.WithTimeout(ctx, timeout) } else { ctx, cancel = context.WithCancel(ctx) } e.Defer(cancel) return ctx } http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { var err error e := errc.Catch(&err) defer e.Handle() ctx := contextWithTimeout(&e, req) do(ctx) }) }
Output:
type Handler ¶
type Handler interface { // Handle processes an error detected by Must or Defer. It may replace the // error with another one, return it as is, or return nil, in which case // error handling is terminated and Must or Defer will continue operation // as if the error did not occur. Handle(s State, err error) error }
A Handler processes errors.
Example (Fatal) ¶
package main import ( "errors" "io" "io/ioutil" "strings" "github.com/mpvl/errc" ) func main() { func() (err error) { e := errc.Catch(&err, errc.Fatal) defer e.Handle() r, err := newReader() e.Must(err) e.Defer(r.Close) r, err = newFaultyReader() e.Must(err) e.Defer(r.Close) return nil }() } func newReader() (io.ReadCloser, error) { return ioutil.NopCloser(strings.NewReader("Hello World!")), nil } func newFaultyReader() (io.ReadCloser, error) { return nil, errors.New("errd_test: error") }
Output:
var ( // Discard is a handler that discards the given error, causing // normal control flow to resume. Discard Handler = HandlerFunc(discard) // Fatal is handler that causes execution to halt. Fatal Handler = HandlerFunc(fatal) )
type HandlerFunc ¶
The HandlerFunc type is an adapter to allow the use of ordinary functions as error handlers. If f is a function with the appropriate signature, HandlerFunc(f) is a Handler that calls f.
type State ¶
type State interface { // Panicking reports whether the error resulted from a panic. If true, // the panic will be resume after error handling completes. An error handler // cannot rewrite an error when panicing. Panicking() bool // Err reports the first error that passed through an error handler chain. // Note that this is always a different error (or nil) than the one passed // to an error handler. Err() error }
State represents the error state passed to custom error handlers.