errc

package module
v0.0.0-...-1ae3d10 Latest Latest
Warning

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

Go to latest
Published: Nov 8, 2017 License: BSD-3-Clause Imports: 5 Imported by: 2

README

errc GoDoc Travis-CI Report card codecov

Package errc simplifies error and defer handling.

go get github.com/mpvl/errc

Also note the sibling package go get github.com/mpvl/errd. Package errc probably looks more like a language feature would look like. Package errd however is a bit safer to use as well as a bit faster.

Overview

Package errc is a burner package, a proof-of-concept to analyze how to improve error handling for future iterations of Go. The idiomatic way to handle errors in Go looks like:

    func foo() (err error) {
        r, err := getReader()
        if err != nil {
            return err
        }
        defer r.Close()
        // etc.

The implied promise of simplicity of this pattern, though a bit verbose, often does not hold. Take the following example:

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
}

This function atomically writes the contents of an io.Reader to a Google Cloud Storage file. It ensures the following:

  1. An error resulting from closing w is returned if there wasn't any error already
  2. In case of a panic, neither Close() nor CloseWithError(nil) is called.

The first condition is necessary to ensure any retry logic will know the file was not successfully written. The second condition ensures no partial file is written in case of a panic. A panic may occur, for instance, when the server is killed by a cluster manager because it uses too much memory.

Using package errc, the same is achieved by:

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, errc.Discard)

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    e.Defer(w.CloseWithError)

    _, err = io.Copy(w, r)
    e.Must(err)
}

In this case, the above guarantees are met by applying the idiomatic check-and-defer pattern. The handling of errors around panics, Must and Defer is such that applying the check-and-defer pattern yields the correct results without much further thought.

Error Handlers

Package errc defines a Handler type to allow inline processing of 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) error {
    e := errc.Catch(&err)
    defer e.Handle()

    client, err := storage.NewClient(ctx)
    e.Must(err, msg("creating client failed"))
    e.Defer(client.Close, errc.Discard)

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    e.Defer(w.CloseWithError)

    _, err = io.Copy(w, r)
    e.Must(err, msg("copy failed"))
    return nil
}

It is also possible to pass a default Handler to the Catch function, which will be applied if no Handler is given at the point of detection.

Principles

As said, errc is a "burner package". The goal is to improve error handling focussing on semantics first, rather than considering syntax first.

The main requirements for error handling addressed by errc are:

  • errors are and remain values
  • Make it easy to decorate errors with additional information (and play nice with packages like github.com/pkg/errors).
  • Using an idiomatic way to handling errors should typically result in correct behavior.

Error funnel

The main errc concept is that of an error funnel: a single variable associated with each function in which the current error state is recorded. It is very much like having a named error return argument in which to record all errors, but ensuring that the following holds:

  • there is a single error variable,
  • an error detected by a call to Must or Defer will only be recorded in the error variable if the error variable is nil,
  • some processing is allowed unconditionally for any error that is detected,
  • if a panic occurs, the current error variable will be overwritten by a wrapped panic error, and
  • it is still possible to override any previous error value by explicitly writing to the error variable.

Errors versus Panics

One could classify error values as recoverable errors while panics are unrecoverable errors. In practice things are a bit more subtle. Cleanup code that is called through defers is still called after a panic. Although easily ignored, it may be important for such code to consider a panic as being in an erroring state. However, by default in Go panic and errors are treated completely separately. Package errc preserves panic semantics, while also treating panics as an error. More specifically, a panic will keep unwinding until an explicit recover, while at the same time it assigns a panic-related error to the error variable to communicate that the function is currently in an erroring state.

How it works

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

Performance

Package errc 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 errc 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.

Caveat Emptor

As errc uses defer, it does not work across goroutine boundaries. In general, it is advisable not to pass an errc.Catcher value as an argument to any function call.

What's next

Package errc is about exploring better ways and semantics for handling errors and defers. The main goal here is to come up with a good improvement for Go 2.

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

func Catch(err *error, h ...Handler) Catcher

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

func (e *Catcher) 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)

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:

func (*Catcher) Handle

func (e *Catcher) Handle()

Handle manages the error handling and defer processing. It must be called after any call to Catch.

func (*Catcher) Must

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

Must causes a return from a function if err is not nil, and after the error is not nullified by any of the Handlers.

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

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

Jump to

Keyboard shortcuts

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