errd

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 8, 2017 License: BSD-3-Clause Imports: 6 Imported by: 0

README

errd GoDoc Travis-CI Report card codecov

Package errd simplifies error and defer handling.

go get github.com/mpvl/errd

Overview

A common pattern in Go after creating new value is to check for any error and then to call the cleanup method or function for this value immediately following this check using defer:

w, err := NewWriter()
if err != nil {
    return err
}
defer w.Close()

To some Go programmers this is too much typing and several proposals to add language support for handling errors have been made. At the same time, users are encouraged to add information to each point at which an error is encountered. Many language proposals make it harder to decorate errors, not easier.

Another issue with the idiomatic approach to error handling is that it is not as straightforward as is it may seem. For example, it is not uncommon to ignore the error returned by close, even though it may return useful information. Things get even more hairy if it becomes important to pass an error to a defer or if panics need to be caught. Package errd aims to streamlining and simplifying defer handling, making it easier to wrap errors, all while reducing the amount of typing.

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. Note that, in this case, we chose to ignore the error of the first close: once the second Close is called successfully, the write operation completed successfully and we consider further errors irrelevant.

This example shows that error handling can be subtle and not the mindless check-decorate-return pattern it often seems to be. Does a Closer also support CloseWithError? Which panics need to be handled? Also note the subtle use of the return variable to convey an error in Copy to the last defer function.

In package errd the same code is written as:

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

Run starts a scope in which errors and defers are managed. The Must forces the closure called by Run to return immediately if the passed error is not nil. The Defer methods implement a conservative handling of errors, passing along any caught error when possible.

Error Handlers

In all of the above code 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"))
    })
}

Instead of handling individual cases, we can also define a more generic storage error handle to add information based on the returned error. This code uses such a default handler:

var ecGS = errd.WithDefault(convertGSError)

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

The use of default handlers makes it easier to ensure an error is always handled.

How it works

Package errd uses Go's panic and recover mechanism to force the exit from Run if an error is encountered. On top of that, package errd manages its own defer state, which is necessary to properly interweave error and defer handling.

Naming

The name of the package suggests this is a daemon for handling errors. The package is clearly not a daemon. However, errd does suggests that it is does something with errors related to handling them in the background. But that is pretty much what the package does and it is short to boot. The errd name originally popped in my head as a result of finding contractions of error and defer. So if the daemon implication bothers you, think of the d standing for defer.

Must is idiomatically used in Go to indicate code will panic, and thus abort the usual flow of execution, if an error is encountered. We piggyback on this convention to signal that the execution of the closure passed to Run will be interrupted if a non-nil error is passed to Must. Where package errd differs from the convention is that it catches the panic created by Must and converts it to an error when Run returns. A similar effect can be achieved for a Must of any other package by catching the panic with a defer an recover. As such package errd merely expands this convention.

Performance

Package errd adds a defer block to do all its management. If the original code only does error checking, this is a relatively big price to pay. If the original code already does a defer, the damage is limited. If the original code uses multiple defers, package errd may even be faster.

Passing string-type error handlers, like in the example on error handlers, causes an allocation. However, in 1.9 this special case does not incur noticeable overhead over passing a pre-allocated handler.

Design Philosophy

The package aims to simplify error and defer handling now, with an ulterior motive to gather enough data to aid making decisions on how to improve error and defer handling in Go 2.

Some design guidelines used in the development of this package:

  • Stay true to the "error are values" principle.
  • Make the most conservative error handling the default, and allow users to be explicit about relaxing the rules.
  • Panics stay panics: although we use the panic mechanism, do not let the user use this mechanism to bypass panics.
  • Stay compatible with existing packages for extending and adorning errors (like github.com/pkg/errors).
  • Performance: don't do things that impact performance too much: allow as many people as possible to use the package now, versus relying on possible compiler improvements down the road.

What's next

Package errd is about exploring better ways and semantics for handling errors and defers with an aim to improve Go 2.

NOTE: the rest of this section is just brainstorming and is no indication of where Go is heading.

For example, The approach of package errd interacts well with generics.

Consider the methods:

func [T] (e *E) Defer(x T, h ... Handler) T
func [T] (e *E) MustD1(x T, err error, h ... Handler) T

Where MustD1 checks the error and returns and the first argument after first adding the defer handler for this argument. The writeToGS function from above can now be written as:

func writeToGS(ctx context.Context, bkt, dst, src string) error {
    return errd.Run(func(e *errd.E) {
        client := e.MustD1(storage.NewClient(ctx), msg("error opening client"))
        _ = e.Must1(io.Copy(e.Defer(client.Bucket(bkt).Object(dst).NewWriter(ctx)), r))
    })
}

Syntactic sugar could further simplify things:

func writeToGS(ctx context.Context, bkt, dst, src string) error {
    return catch(handler) {
        client := storage.close#must#NewClient(ctx)
        _ = io.must#Copy(client.Bucket(bkt).Object(dst).close#NewWriter(ctx), r)
    })
}

where must checks the error and bails if non-nil, close calls a defer func that picks Close and CloseWithError. Both must and close could be user-defined wrapper functions.

A big advantage of making this a language feature is that it would be easier to enforce that this feature is not used across function, or even worse, goroutine boundaries.

https://blog.golang.org/errors-are-values

https://research.swtch.com/go2017#error

https://github.com/golang/go/issues/21161 poses as a problem what is addressed in this package.

In https://github.com/golang/go/issues/20803 argues against allowing implicitly ignored variables. Package errd approach tackles a similar sentiment by making it more likely an error is handled by default and requiring more intervention to ignore an error.

Issue https://github.com/golang/go/issues/18721 aims to be terser by introducing syntactic sugar for error handling. Error handling as done by package errd, though terser, is clearly not as terse. However, the hope is that building experience with the use of package errd helps making the right decision on what kind of language changes are appropriate. One of the cons mentioned in this issue is that it may encourage bad practices such as not decorating errors with additional information. Package errd makes such decoration easier, not harder.

Issue https://github.com/golang/go/issues/16225 also expresses the desire to do shorter and terser error handling. Package errd accomplishes this without a language change.

Issue https://github.com/golang/go/issues/21182 aims to reduce noise in return statements when returning non-nil errors. This is addressed by package errd by eliminating the returns in such cases alltogether.

Some other related Issues: https://github.com/golang/go/issues/19511 https://github.com/golang/go/issues/19727 https://github.com/golang/go/issues/20148

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

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

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!

func RunWithContext

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

RunWithContext calls Default.RunWithContext(ctxt, f)

Types

type E

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

An E coordinates the error and defer handling.

func (*E) Defer

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.

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:

func (*E) Must

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

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

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

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

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

Handle calls f(s, err).

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

func WithDefault(h ...Handler) *Runner

WithDefault returns a new Config for the given default handlers.

func (*Runner) Run added in v0.3.0

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 added in v0.3.0

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

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL