Marcel van Lohuizen
August 27, 2018
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.
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.
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.
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.
The error printing API should allow
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:
fmt.Formatter
.errors.Printer
to allow translation of messages.errors.Printer
to produce formats.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.
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 syntaxThis design defines %+v
to print the error in the detailed, multi-line format.
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(), } }
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.
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
We considered defining multiple optional methods, to provide fine-grained information such as the underlying error, detailed message, etc. This had many drawbacks:
fmt.Formatter
to correctly handle print verbs, which was cumbersome and led to inconsistencies and incompatibilities.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.
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.
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.