errd: github.com/mpvl/errd Index | Examples | Files

package errd

import "github.com/mpvl/errd"

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

Package Files

defer.go doc.go errd.go handler.go

func Run Uses

func Run(f func(*E)) (err error)

Run calls Default.Run(f)

Code:

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!

Code:

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!

func RunWithContext Uses

func RunWithContext(ctxt context.Context, f func(*E)) (err error)

RunWithContext calls Default.RunWithContext(ctxt, f)

type E Uses

type E struct {
    // contains filtered or unexported fields
}

An E coordinates the error and defer handling.

func (*E) Defer Uses

func (e *E) Defer(x interface{}, h ...Handler)

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.

ExampleE_Defer_cancelHelper shows how a helper function may call a defer in the caller's E.

Code:

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)
    })
})

func (*E) Must Uses

func (e *E) Must(err error, h ...Handler)

Must causes a call to Run to return on error. An error is detected if err is non-nil and if it is still non-nil after passing it to error handling.

type Handler Uses

type Handler interface {
    Handle(s State, err error) error
}

A Handler processes errors.

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)
)

Code:

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)
})

type HandlerFunc Uses

type HandlerFunc func(s State, err error) error

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.

func (HandlerFunc) Handle Uses

func (f HandlerFunc) Handle(s State, err error) error

Handle calls f(s, err).

type Runner Uses

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 Uses

func WithDefault(h ...Handler) *Runner

WithDefault returns a new Config for the given default handlers.

func (*Runner) Run Uses

func (r *Runner) Run(f func(e *E)) (err error)

Run starts a new error handling scope. The function returns whenever an error is encountered with one of the methods on E.

func (*Runner) RunWithContext Uses

func (r *Runner) RunWithContext(ctxt context.Context, f func(e *E)) (err error)

RunWithContext starts a new error handling scope. The function returns whenever an error is encountered with one of the methods on E.

type State Uses

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.

Package errd imports 6 packages (graph). Updated 2018-05-10. Refresh now. Tools for package owners.