Documentation ¶
Overview ¶
Package errd simplifies error and defer handling.
Overview ¶
Package errd allows returning form a block of code without using the usual if clauses, while properly intercepting errors and passing them to code called at defer time.
The following piece of idiomatic Go writes the contents of a reader to a file on Google Cloud Storage:
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) defer func() { if r := recover(); r != nil { w.CloseWithError(fmt.Errorf("panic: %v", r)) panic(r) } if err != nil { _ = w.CloseWithError(err) } else { err = w.Close() } } _, err = io.Copy(w, r) return err }
Google Cloud Storage allows files to be written atomically. This code minimizes the chance of writing a bad file by aborting the write, using CloseWithError, whenever any anomaly is encountered. This includes a panic that could occur in the reader.
Package errd aims to reduce bugs resulting from such subtleties by making the default of having very strict error checking easy. The following code achieves the same as the above:
func writeToGS(ctx context.Context, bucket, dst, src string, r io.Reader) error { return errd.Run(func(e *errd.E) { 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) e.Must(err) }) }
Discard is an example of an error handler. Here it signals that we want to ignore the error of the first Close.
Error Handlers ¶
In all of the code above we made the common faux pas of passing errors on without decorating them. Package errd defines a Handler type to simplify the task of decorating.
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 errd.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) error { return errd.Run(func(e *errd.E) { 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")) }) }
The storage package used in this example defines the errors that are typically a result of user error. It would be possible to write a more generic storage writer that will add additional clarification when possible. Using such a handler as a default handler would look like:
var ecGS = errd.WithDefault(storageHandler) func writeToGS(ctx context.Context, bucket, dst, src string) error { return ecGS.Run(func(e *errd.E) { client, err := storage.NewClient(ctx) e.Must(err) e.Defer(client.Close) ... }) }
Setting up a global config with a default handler and using that everywhere makes it easy to enforce decorating errors. Error handlers can also be used to pass up HTTP error codes, log errors, attach metrics, etc.
Returning Values ¶
A function that is passed to Run does have any return values, not even an error. Users are supposed to set return values in the outer scope, for example by using named return variables.
func foo() (name string, err error) { return name, Run(func(e *errd.E) { // Some fun code here. // Set name at the end. name = "bar" }) }
If name is only set at the end, any error will force an early return from Run and leave name empty. Otherwise, name will be set and err will be nil.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Run ¶
Run calls Default.Run(f)
Example ¶
package main import ( "io" "io/ioutil" "os" "strings" "github.com/mpvl/errd" ) func newReader() (io.ReadCloser, error) { return ioutil.NopCloser(strings.NewReader("Hello World!")), nil } func main() { errd.Run(func(e *errd.E) { r, err := newReader() // contents: Hello World! e.Must(err) e.Defer(r.Close) _, err = io.Copy(os.Stdout, r) e.Must(err) }) }
Output: Hello World!
Example (Pipe) ¶
package main import ( "io" "io/ioutil" "os" "strings" "github.com/mpvl/errd" ) func newReader() (io.ReadCloser, error) { return ioutil.NopCloser(strings.NewReader("Hello World!")), nil } func main() { r, w := io.Pipe() go errd.Run(func(e *errd.E) { e.Defer(w.CloseWithError) r, err := newReader() // contents: Hello World! e.Must(err) e.Defer(r.Close) _, err = io.Copy(w, r) e.Must(err) }) io.Copy(os.Stdout, r) // The above goroutine is equivalent to: // // go func() { // var err error // used to intercept downstream errors // defer w.CloseWithError(err) // // r, err := newReader() // if err != nil { // return // } // defer func() { // if errC := r.Close(); errC != nil && err == nil { // err = errC // } // } // // _, err = io.Copy(w, r) // }() }
Output: Hello World!
Types ¶
type E ¶
type E struct {
// contains filtered or unexported fields
}
An E coordinates the error and defer handling.
func (*E) 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) ¶
ExampleE_Defer_cancelHelper shows how a helper function may call a defer in the caller's E.
package main import ( "context" "net/http" "time" "github.com/mpvl/errd" ) func do(ctx context.Context) { } func main() { contextWithTimeout := func(e *errd.E, 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) { errd.Run(func(e *errd.E) { ctx := contextWithTimeout(e, req) do(ctx) }) }) }
Output:
type Handler ¶
A Handler processes errors.
Example (Fatal) ¶
package main import ( "errors" "io" "io/ioutil" "strings" "github.com/mpvl/errd" ) func main() { exitOnError := errd.WithDefault(errd.Fatal) exitOnError.Run(func(e *errd.E) { r, err := newReader() e.Must(err) e.Defer(r.Close) r, err = newFaultyReader() e.Must(err) e.Defer(r.Close) }) } 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 Runner ¶ added in v0.3.0
type Runner struct {
// contains filtered or unexported fields
}
A Runner defines a default way to handle errors and options.
var (
Default *Runner = WithDefault()
)
Default is the default Runner comfiguration.
func WithDefault ¶ added in v0.3.0
WithDefault returns a new Config for the given default handlers.
type State ¶
type State interface { // Context returns the context set by WithContext, or context.TODO // otherwise. Context() context.Context // 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.