go / errwriter

I use a sticky error writer to simplify code that makes many sequential writes, checking for errors only at the end.

The problem

Writing to an io.Writer requires checking errors after each call:

func writeRecord(w io.Writer, r Record) error {
	_, err := w.Write(r.Header)
	if err != nil {
		return err
	}
	_, err = w.Write(r.Body)
	if err != nil {
		return err
	}
	_, err = w.Write(r.Footer)
	if err != nil {
		return err
	}
	return nil
}

This is tedious and obscures the actual logic.

The pattern

Wrap the writer to remember the first error and skip subsequent writes:

package errwriter

import "io"

// Writer wraps an io.Writer, recording the first error.
// After an error, all writes become no-ops.
type Writer struct {
	w   io.Writer
	n   int64
	err error
}

func New(w io.Writer) *Writer {
	return &Writer{w: w}
}

func (w *Writer) Write(p []byte) (int, error) {
	if w.err != nil {
		return 0, w.err
	}
	n, err := w.w.Write(p)
	w.n += int64(n)
	w.err = err
	return n, err
}

// Err returns the first error encountered, or nil.
func (w *Writer) Err() error {
	return w.err
}

// Written returns the total bytes successfully written.
func (w *Writer) Written() int64 {
	return w.n
}

Usage

Now the writing code is clean:

func writeRecord(w io.Writer, r Record) error {
	ew := errwriter.New(w)
	ew.Write(r.Header)
	ew.Write(r.Body)
	ew.Write(r.Footer)
	return ew.Err()
}

Works well with fmt.Fprintf:

func writeReport(w io.Writer, data []Row) error {
	ew := errwriter.New(w)
	fmt.Fprintf(ew, "Report: %s\n", time.Now().Format(time.RFC3339))
	fmt.Fprintf(ew, "Rows: %d\n\n", len(data))
	for _, row := range data {
		fmt.Fprintf(ew, "%s\t%d\n", row.Name, row.Value)
	}
	return ew.Err()
}

When to use

Attribution

This is Rob Pike's "errors are values" pattern from the Go Blog.

← All articles