Error Printing — Draft Design

Marcel van Lohuizen
August 27, 2018

Abstract

This document is a draft design for additions to the errors package to define defaults for formatting error messages, with the aim of making formatting of different error message implementations interoperable. This includes the printing of detailed information, stack traces or other position information, localization, and limitations of ordering.

For more context, see the error values problem overview.

Background

It is common in Go to build your own error type. Applications can define their own local types or use one of the many packages that are available for defining errors.

Broadly speaking, errors serve several audiences: programs, users, and diagnosers. Programs may need to make decisions based on the value of errors. This need is addressed in the error values draft designs. Users need a general idea of what went wrong. Diagnosers may require more detailed information. This draft design focuses on providing legible error printing to be read by people—users and diagnosers—not programs.

When wrapping one error in context to produce a new error, some error packages distinguish between opaque and transparent wrappings, which affect whether error inspection is allowed to see the original error. This is a valid distinction. Even if the original error is hidden from programs, however, it should typically still be shown to people. Error printing therefore must use an interface method distinct from the common “next in error chain” methods like Cause, Reason, or the error inspection draft design’s Unwrap.

There are several packages that have attempted to provide common error interfaces. These packages typically do not interoperate well with each other or with bespoke error implementations. Although the interfaces they define are similar, there are implicit assumptions that lead to poor interoperability.

Design

This design focuses on printing errors legibly, for people to read. This includes possible stack trace information, a consistent ordering, and consistent handling of formatting verbs.

Error detail

The design allows for an error message to include additional detail printed upon request, by using special formatting verb %+v. This detail may include stack traces or other detailed information that would be reasonable to elide in a shorter display. Of course, many existing error implementations only have a short display, and we don’t expect them to change. But implementations that do track additional detail will now have a standard way to present it.

Printing API

The error printing API should allow

  • consistent formatting and ordering,
  • detailed information that is only printed when requested (such as stack traces),
  • defining a chain of errors (possibly different from “reasons” or a programmatic chain),
  • localization of error messages, and
  • a formatting method that is easy for new error implementations to implement.

The design presented here introduces two interfaces to satisfy these requirements: Formatter and Printer, both defined in the errors package.

An error that wants to provide additional detail implements the errors.Formatter interface’s Format method.

The Format method is passed an errors.Printer, which itself has Print and Printf methods.

package errors

// A Formatter formats error messages.
type Formatter interface {
	// Format is implemented by errors to print a single error message.
	// It should return the next error in the error chain, if any.
	Format(p Printer) (next error)
}

// A Printer creates formatted error messages. It enforces that
// detailed information is written last.
//
// Printer is implemented by fmt. Localization packages may provide
// their own implementation to support localized error messages
// (see for instance golang.org/x/text/message).
type Printer interface {
	// Print appends args to the message output.
	// String arguments are not localized, even within a localized context.
	Print(args ...interface{})

	// Printf writes a formatted string.
	Printf(format string, args ...interface{})

	// Detail reports whether error detail is requested.
	// After the first call to Detail, all text written to the Printer
	// is formatted as additional detail, or ignored when
	// detail has not been requested.
	// If Detail returns false, the caller can avoid printing the detail at all.
	Detail() bool
}

The Printer interface is designed to allow localization. The Printer implementation will typically be supplied by the fmt package but can also be provided by localization frameworks such as golang.org/x/text/message. If instead a Formatter wrote to an io.Writer, localization with such packages would not be possible.

In this example, myAddrError implements Formatter: Example:

type myAddrError struct {
	address string
	detail  string
	err     error
}

func (e *myAddrError) Error() string {
	return fmt.Sprint(e) // delegate to Format
}

func (e *myAddrError) Format(p errors.Printer) error {
	p.Printf("address %s", e.address)
	if p.Detail() {
		p.Print(e.detail)
	}
	return e.err
}

This design assumes that the fmt package and localization frameworks will add code to recognize errors that additionally implement Formatter and use that method for %+v. These packages already recognize error; recognizing Formatter is only a little more work.

Advantages of this API:

  • This API clearly distinguishes informative detail from a causal error chain, giving less rise to confusion.
  • Consistency between different error implementations:
    • interpretation of formatting flags
    • ordering of the error chain
    • formatting and indentation
  • Less boilerplate for custom error types to implement:
    • only one interface to implement besides error
    • no need to implement fmt.Formatter.
  • Flexible: no assumption about the kind of detail information an error implementation might want to print.
  • Localizable: packages like golang.org/x/text/message can provide their own implementation of errors.Printer to allow translation of messages.
  • Detail information is more verbose and somewhat discouraged.
  • Performance: a single buffer can be used to print an error.
  • Users can implement errors.Printer to produce formats.

Format

Consider an error that returned by foo calling bar calling baz. An idiomatic Go error string would be:

foo: bar(nameserver 139): baz flopped

We suggest the following format for messages with diagnostics detail, assuming that each layer of wrapping adds additional diagnostics information.

foo:
    file.go:123 main.main+0x123
--- bar(nameserver 139):
    some detail only text
    file.go:456
--- baz flopped:
    file.go:789

This output is somewhat akin to that of subtests. The first message is printed as formatted, but with the detail indented with 4 spaces. All subsequent messages are indented 4 spaces and prefixed with --- and a space at the start of the message.

Indenting the detail of the first message avoids ambiguity when multiple multiline errors are printed one after the other.

Formatting verbs

Today, fmt.Printf already prints errors using these verbs:

  • %s: err.Error() as a string
  • %q: err.Error() as a quoted string
  • %+q: err.Error() as an ASCII-only quoted string
  • %v: err.Error() as a string
  • %#v: err as a Go value, in Go syntax

This design defines %+v to print the error in the detailed, multi-line format.

Interaction with source line information

The following API shows how printing stack traces, either top of the stack or full stacks per error, could interoperate with this package (only showing the parts of the API relevant to this discussion).

package errstack

type Stack struct { ... }

// Format writes the stack information to p, but only
// if detailed printing is requested.
func (s *Stack) Format(p errors.Printer) {
	if p.Detail() {
		p.Printf(...)
	}
}

This package would be used by adding a Stack to each error implementation that wanted to record one:

import ".../errstack"

type myError struct {
	msg        string
	stack      errstack.Stack
	underlying error
}

func (e *myError) Format(p errors.Printer) error {
	p.Printf(e.msg)
	e.stack.Format(p)
	return e.underlying
}

func newError(msg string, underlying error) error {
	return &myError{
		msg:   msg,
		stack: errstack.New(),
	}
}

Localized errors

The golang.org/x/text/message package currently has its own implementation of fmt-style formatting. It would need to recognize errors.Formatter and provide its own implementation of errors.Printer with a translating Printf and localizing Print.

import "golang.org/x/text/message"

p := message.NewPrinter(language.Dutch)
p.Printf("Error: %v", err)

Any error passed to %v that implements errors.Formatter would use the localization machinery. Only format strings passed to Printf would be translated, although all values would be localized. Alternatively, since errors are always text, we could attempt to translate any error message, or at least to have gotext do static analysis similarly to what it does now for regular Go code.

To facilitate localization, golang.org/x/text/message could implement an Errorf equivalent which delays the substitution of arguments until it is printed so that it can be properly localized.

The gotext tool would have to be modified to extract error string formats from code. It should be easy to modify the analysis to pick up static error messages or error messages that are formatted using an errors.Printer's Printf method. However, calls to fmt.Errorf will be problematic, as it substitutes the arguments prematurely. We may be able to change fmt.Errorf to evaluate and save its arguments but delay the final formatting.

Error trees

So far we have assumed that there is a single chain of errors. To implement formatting a tree of errors, an error list type could print itself as a new error chain, returning this single error with the entire chain as detail. Error list types occur fairly frequently, so it may be beneficial to standardize on an error list type to ensure consistency.

The default output might look something like this:

foo: bar: baz flopped (and 2 more errors)

The detailed listing would show all the errors:

foo:
--- multiple errors:
    bar1
    --- baz flopped
    bar2
    bar3

Alternate designs

We considered defining multiple optional methods, to provide fine-grained information such as the underlying error, detailed message, etc. This had many drawbacks:

  • Implementations needed to implement fmt.Formatter to correctly handle print verbs, which was cumbersome and led to inconsistencies and incompatibilities.
  • It required having two different methods returning the “next error” in the wrapping chain: one to report the next for error inspection and one to report the next for printing. It was difficult to remember which was which.
  • Error implementations needed too many methods.
  • Most such approaches were incompatible with localization.

We also considered hiding the Formatter interface in the fmt.State implementation. This was clumsy to implement and it shared the drawback of requiring error implementation authors to understand how to implement all the relevant formatting verbs.

Migration

Packages that currently do their own formatting will have to be rewritten to use the new interfaces to maximize their utility. In experimental conversions of github.com/pkg/errors, gopkg.in/errgo.v2, and upspin.io/errors, we found that implementing Formatter simplified printing logic considerably, with the simultaneous benefit of making chains of these errors interoperable.

This design’s detailed, multiline form is always an expansion of the single-line form, proceeding through in the same order, outermost to innermost. Other packages, like github.com/pkg/errors, conventionally print detailed errors in the opposite order, contradicting the single-line form. Users used to reading those errors will need to learn to read the new format.

Disadvantages

The approach presented here does not provide any standard to programmatically extract the information that is to be displayed in the messages. It seems, though, there is no need for this. The goal of this approach is interoperability and standardization, not providing structured access.

As noted in the previous section, existing error packages that print detail will need to update their formatting implementations, and some will find that the reporting order of errors has changed.

This approach does not specify a standard for printing trees. Providing a standard error list type could help with this.