blob: 7570ca9901c52f70b15ddfa8c3edcd6eae5acffd [file] [log] [blame] [view]
# Error Values — Problem Overview
Russ Cox\
August 27, 2018
## Introduction
This overview and the accompanying
detailed draft designs
are part of a collection of [Go 2 draft design documents](go2draft.md).
The overall goal of the Go 2 effort is to address
the most significant ways that Go fails to scale
to large code bases and large developer efforts.
One way that Go programs fail to scale well is in the capability of typical errors.
A variety of popular helper packages add functionality
beyond the standard error interface,
but they do so in incompatible ways.
As part of Go 2, we are considering whether to standardize any
"optional interfaces" for errors,
to allow helper packages to interoperate
and ideally to reduce the need for them.
As part of Go 2, we are also considering,
as a separate concern,
more convenient [syntax for error checks and handling](go2draft-error-handling-overview.md).
## Problem
Large programs must be able to test for
and react to errors programmatically and also report them well.
Because an error value is any value implementing the [`error` interface](https://golang.org/ref/spec#Errors),
there are four ways that Go programs conventionally test for specific errors.
First, programs can test for equality with sentinel errors like `io.EOF`.
Second, programs can check for an error implementation type using a [type assertion](https://golang.org/ref/spec#Type_assertions) or [type switch](https://golang.org/ref/spec#Type_switches).
Third, ad-hoc checks like
[`os.IsNotExist`](https://golang.org/pkg/os/#IsNotExist)
check for a specific kind of error,
doing limited unwrapping.
Fourth, because neither of these approaches works in general when the error has been wrapped in additional context,
programs often do substring searches in the error text reported by `err.Error()`.
Obviously this last approach is the least desirable,
and it would be better to support the first three checks even in the presence of arbitrary wrapping.
The most common kind of wrapping is use of fmt.Errorf, as in
if err != nil {
return fmt.Errorf("write users database: %v", err)
}
Wrapping in an error type is more work but more useful for programmatic tests, like in:
if err != nil {
return &WriteError{Database: "users", Err: err}
}
Either way, if the original `err` is a known sentinel or known error implementation type,
then wrapping, whether by `fmt.Errorf` or a new type like `WriteError`,
breaks both equality checks and type assertions looking for the original error.
This discourages wrapping, leading to less useful errors.
In a complex program, the most useful description of an error
would include information about all the different operations leading to the error.
For example, suppose that the error writing to the database
earlier was due to an RPC call.
Its implementation called `net.Dial` of `"myserver"`,
which in turn read `/etc/resolv.conf`, which maybe today was accidentally unreadable.
The resulting errors `Error` method might return this string (split onto two lines for this document):
write users database: call myserver.Method: \
dial myserver:3333: open /etc/resolv.conf: permission denied
The implementation of this error is five different levels (four wrappings):
1. A `WriteError`, which provides `"write users database: "` and wraps
2. an `RPCError`, which provides `"call myserver.Method: "` and wraps
3. a `net.OpError`, which provides `"dial myserver:3333: "` and wraps
4. an `os.PathError`, which provides `"open /etc/resolv.conf: "` and wraps
5. `syscall.EPERM`, which provides `"permission denied"`
There are many questions you might want to ask programmatically of err,
including:
(i) is it an RPCError?
(ii) is it a net.OpError?
(iii) does it satisfy the net.Error interface?
(iv) is it an os.PathError?
(v) is it a permission error?
The first problem is that it is too hard to ask these kinds of questions.
The functions [`os.IsExist`](https://golang.org/pkg/os/#IsExist),
[`os.IsNotExist`](https://golang.org/pkg/os/#IsNotExist),
[`os.IsPermission`](https://golang.org/pkg/os/#IsPermission),
and
[`os.IsTimeout`](https://golang.org/pkg/os/#IsTimeout)
are symptomatic of the problem.
They lack generality in two different ways:
first, each function tests for only one specific kind of error,
and second, each understands only a very limited number of wrapping types.
In particular, these functions understand a few wrapping errors,
notably [`os.PathError`](https://golang.org/pkg/os/#PathError), but
not custom implementations like our hypothetical `WriteError`.
The second problem is less critical but still important:
the reporting of deeply nested errors is too difficult to read
and leaves no room for additional detail,
like relevant file positions in the program.
Popular helper packages exist to address these problems,
but they disagree on the solutions and in general do not interoperate.
## Goals
There are two goals, corresponding to the two main problems.
First, we want to make error inspection by programs easier
and less error-prone, to improve the error handling and
robustness of real programs.
Second, we want to make it possible to print errors
with additional detail, in a standard form.
Any solutions must keep existing code working
and fit with existing source trees.
In particular, the concepts of comparing for equality with error sentinels
like `io.ErrUnexpectedEOF` and testing for errors of a particular type must be preserved.
Existing error sentinels must continue to be supported,
and existing code will not change to return different error types.
That said, it would be okay to expand functions like
[`os.IsPermission`](https://golang.org/pkg/os/#IsPermission) to understand arbitrary wrappings instead of a fixed set.
When considering solutions for printing additional error detail,
we prefer solutions that make it possibleor at least avoid making it impossibleto
localize and translate errors using
[golang.org/x/text/message](https://godoc.org/golang.org/x/text/message).
Packages must continue to be able to define their own error types easily.
It would be unacceptable to define a new, generalized "one true error implementation"
and require all code to use that implementation.
It would be equally unacceptable to add so many additional requirements
on error implementations that only a few packages would bother.
Errors must also remain efficient to create.
Errors are not exceptional.
It is common for errors to be generated, handled, and discarded,
over and over again, as a program executes.
As a cautionary tale, years ago at Google a program written
in an exception-based language was found to be spending
all its time generating exceptions.
It turned out that a function on a deeply-nested stack was
attempting to open each of a fixed list of file paths,
to find a configuration file.
Each failed open operation threw an exception;
the generation of that exception spent a lot of time recording
the very deep execution stack;
and then the caller discarded all that work and continued around its loop.
The generation of an error in Go code must remain a fixed cost,
regardless of stack depth or other context.
(In a panic, deferred handlers run _before_ stack unwinding
for the same reason: so that handlers that do care about
the stack context can inspect the live stack,
without an expensive snapshot operation.)
## Draft Design
The two main problemserror inspection and error formattingare
addressed by different draft designs.
The constraints of keeping interoperation with existing code
and allowing packages to continue to define their own error types
point strongly in the direction of defining
optional interfaces that an error implementation can satisfy.
Each of the two draft designs adds one such interface.
### Error inspection
For error inspection, the draft design follows the lead of existing packages
like [github.com/pkg/errors](https://github.com/pkg/errors)
and defines an optional interface for an error to return the next error
in the chain of error wrappings:
package errors
type Wrapper interface {
Unwrap() error
}
For example, our hypothetical `WriteError` above would need to implement:
func (e *WriteError) Unwrap() error { return e.Err }
Using this method, the draft design adds two new functions to package errors:
// Is reports whether err or any of the errors in its chain is equal to target.
func Is(err, target error) bool
// As checks whether err or any of the errors in its chain is a value of type E.
// If so, it returns the discovered value of type E, with ok set to true.
// If not, it returns the zero value of type E, with ok set to false.
func As(type E)(err error) (e E, ok bool)
Note that the second function has a type parameter, using the
[contracts draft design](go2draft-generics-overview.md).
Both functions would be implemented as a loop first testing
`err`, then `err.Unwrap()`, and so on, to the end of the chain.
Existing checks would be rewritten as needed to be "wrapping-aware":
errors.Is(err, io.ErrUnexpectedEOF) // was err == io.ErrUnexpectedEOF
pe, ok := errors.As(*os.PathError)(err) // was pe, ok := err.(*os.PathError)
For details, see the [error inspection draft design](go2draft-error-inspection.md).
### Error formatting
For error formatting, the draft design defines an optional interface implemented by errors:
package errors
type Formatter interface {
Format(p Printer) (next error)
}
The argument to `Format` is a `Printer`, provided by the package formatting the error
(usually [`fmt`](https://golang.org/pkg/fmt), but possibly a localization package like
[`golang.org/x/text/message`](https://godoc.org/golang.org/x/text/message) instead).
The `Printer` provides methods `Print` and `Printf`, which emit output,
and `Detail`, which reports whether extra detail should be printed.
The `fmt` package would be adjusted to format errors printed using `%+v` in a multiline format,
with additional detail.
For example, our database `WriteError` might implement the new `Format` method and the old `Error` method as:
func (e *WriteError) Format(p errors.Printer) (next error) {
p.Printf("write %s database", e.Database)
if p.Detail() {
p.Printf("more detail here")
}
return e.Err
}
func (e *WriteError) Error() string { return fmt.Sprint(e) }
And then printing the original database error using `%+v` would look like:
write users database:
more detail here
--- call myserver.Method:
--- dial myserver:3333:
--- open /etc/resolv.conf:
--- permission denied
The errors package might also provide a convenient implementation
for recording the line number of the code creating the error and printing it back when `p.Detail` returns true.
If all the wrappings involved included that line number information, the `%+v` output would look like:
write users database:
more detail here
/path/to/database.go:111
--- call myserver.Method:
/path/to/grpc.go:222
--- dial myserver:3333:
/path/to/net/dial.go:333
--- open /etc/resolv.conf:
/path/to/os/open.go:444
--- permission denied
For details, see the [error printing draft design](go2draft-error-printing.md).
## Discussion and Open Questions
These draft designs are meant only as a starting point for community discussion.
We fully expect the details to be revised based on feedback and especially experience reports.
This section outlines some of the questions that remain to be answered.
**fmt.Errorf**.
If `fmt.Errorf` is invoked with a format ending in `": %v"` or `": %s"`
and with a final argument implementing the error interface,
then `fmt.Errorf` could return a special implementation
that implements both `Wrapper` and `Formatter`.
Should it? We think definitely yes to `Formatter`.
Perhaps also yes to `Wrapper`, or perhaps we should
introduce `fmt.WrapErrorf`.
Adapting `fmt.Errorf` would make nearly all existing code
using `fmt.Errorf` play nicely with `errors.Is`, `errors.As`,
and multiline error formatting.
Not adapting `fmt.Errorf` would instead require adding
some other API that did play nicely,
for use when only textual context needs to be added.
**Source lines**.
Many error implementations will want to record source lines
to be printed as part of error detail.
We should probably provide some kind of embedding helper
in [package `errors`](https://golang.org/pkg/errors)
and then also use that helper in `fmt.Errorf`.
Another question is whether `fmt.Errorf` should by default
record the file and line number of its caller, for display in the
detailed error format.
Microbenchmarks suggest that recording the callers file and line
number for printing in detailed displays would roughly double the
cost of `fmt.Errorf`, from about 250ns to about 500ns.
**Is versus Last**.
Instead of defining `errors.Is`,
we could define a function `errors.Last`
that returns the final error in the chain.
Then code would write `errors.Last(err) == io.ErrUnexpectedEOF`
instead of `errors.Is(err, io.ErrUnexpectedEOF)`
The draft design avoids this approach for a few reasons.
First, `errors.Is` seems a clearer statement of intent.
Second, the higher-level `errors.Is` leaves room for future adaptation,
instead of being locked into the single equality check.
Even today, the draft designs implementation tests for equality with each error in the chain,
which would allow testing for a sentinel value
that was itself a wrapper (presumably of another sentinel)
as opposed to only testing the end of the error chain.
A possible future expansion might be to allow individual error implementations
to define their own optional `Is(error) bool` methods
and have `errors.Is` prefer that method over the default equality check.
In contrast, using the lower-level idiom
`errors.Last(err) == io.ErrUnexpectedEOF`
eliminates all these possibilities.
Providing `errors.Last(err)` might also encourage type checks
against the result, instead of using `errors.As`.
Those type checks would of course not test against
any of the wrapper types, producing a different result and
introducing confusion.
**Unwrap**. It is unclear if `Unwrap` is the right name for the method
returning the next error in the error chain.
Dave Cheneys [`github.com/pkg/errors`](https://golang.org/pkg/errors) has popularized `Cause` for the method name,
but it also uses `Cause` for the function that returns the last error in the chain.
At least a few people we talked to
did not at first understand the subtle semantic difference between method and function.
An early draft of our design used `Next`, but all our explanations
referred to wrapping and unwrapping,
so we changed the method name to match.
**Feedback**. The most useful general feedback would be
examples of interesting uses that are enabled or disallowed
by the draft design.
Wed also welcome feedback about the points above,
especially based on experience
with complex or buggy error inspection or printing in real programs.
We are collecting links to feedback at
[golang.org/wiki/Go2ErrorValuesFeedback](https://golang.org/wiki/Go2ErrorValuesFeedback).
## Other Go Designs
### Prehistoric Go
The original representation of an error in Go was `*os.Error`, a pointer to this struct:
// Error is a structure wrapping a string describing an error.
// Errors are singleton structures, created by NewError, so their addresses can
// be compared to test for equality. A nil Error pointer means ``no error''.
// Use the String() method to get the contents; it handles the nil case.
// The Error type is intended for use by any package that wishes to define
// error strings.
type Error struct {
s string
}
func NewError(s string) *Error
In April 2009, we changed `os.Error` to be an interface:
// An Error can represent any printable error condition.
type Error interface {
String() string
}
This was the definition of errors in the initial public release,
and programmers learned to use equality tests and type checks to inspect them.
In November 2011, as part of the lead-up to Go 1,
and in response to feedback from Roger Peppe and others in the Go community,
we lifted the interface out of the standard library and into [the language itself](https://golang.org/ref/spec#Errors),
producing the now-ubiquitous error interface:
type error interface {
Error() string
}
The names changed but the basic operations remained the name: equality tests and type checks.
### github.com/spacemonkeygo/errors
[github.com/spacemonkeygo/errors](https://godoc.org/github.com/spacemonkeygo/errors) (July 2013)
was written to support
[porting a large Python codebase to Go](https://medium.com/space-monkey-engineering/go-space-monkey-5f43744bffaa).
It provides error class hierarchies, automatic logging and stack traces, and arbitrary associated key-value pairs.
For error inspection,
it can test whether an error belongs to a particular class, optionally considering wrapped errors,
and considering entire hierarchies.
The `Error` types `Error` method returns a string giving the error class name,
message, stack trace if present, and other data.
There is also a `Message` method that returns just the message,
but there is no support for custom formatting.
### github.com/juju/errgo
[github.com/juju/errgo](https://github.com/juju/errgo) (February 2014)
was written to support Juju, a large Go program developed at Canonical.
When you wrap an error, you can choose whether to
adopt the cause of an underlying error or hide it.
Either way the underlying error is available to printing routines.
The packages `Cause` helper function returns an error intended for the program to act upon,
but it only unwraps one layer,
in contrast to
[`github.com/pkg/errors`](https://godoc.org/github.com/pkg/errors)'s `Cause`
function, which returns the final error in the chain.
The custom error implementations `Error` method concatenates the messages of the errors
along the wrapping chain.
There is also a `Details` function that returns a JSON-like string
with both messages and location information.
### gopkg.in/errgo.v1 and gopkg.in/errgo.v2
[`gopkg.in/errgo.v1`](https://godoc.org/gopkg.in/errgo.v1) (July 2014)
is a slight variation of `github.com/juju/errgo`.
[`gopkg.in/errgo.v2`](https://godoc.org/gopkg.in/errgo.v2)
has the same concepts but a simpler API.
### github.com/hashicorp/errwrap
[`github.com/hashicorp/errwrap`](https://godoc.org/github.com/hashicorp/errwrap) (October 2014)
allows wrapping more than one error, resulting in a general tree of errors.
It has a general `Walk` method that invokes a function on every error in the tree,
as well as convenience functions for matching by type and message string.
It provides no special support for displaying error details.
### github.com/pkg/errors
[`github.com/pkg/errors`](https://godoc.org/github.com/pkg/errors) (December 2015)
provides error wrapping and stack trace capture.
It introduced `%+v` to format errors with additional detail.
The package assumes that only the last error of the chain is of interest,
so it provides a helper `errors.Cause` to retrieve that last error.
It does not provide any functions that consider the entire chain when looking for a match.
### upspin.io/error
[`upspin.io/errors`](https://godoc.org/upspin.io/errors)
is an error package customized for [Upspin](https://upspin.io),
documented in Rob Pike and Andrew Gerrands December 2017 blog post
“[Error handling in Upspin](https://commandcenter.blogspot.com/2017/12/error-handling-in-upspin.html).”
This package is a good reminder of the impact that a custom errors package
can have on a project, and that it must remain easy to implement bespoke
error implementations.
It introduced the idea of `errors.Is`, although the one in the draft design
differs in detail from Upspins.
We considered for a while whether it was possible to adopt
something like Upspins `errors.Match`, perhaps even to generalize
both the draft designs `errors.Is` and `errors.As` into a single
primitive `errors.Match`.
In the end we could not.
## Designs in Other Languages
Most languages do not allow entirely user-defined error implementations.
Instead they define an exception base class that users extend;
the base class provides a common place to hang functionality
and would enable answering these questions in quite a different way.
But of course Go has no inheritance or base classes.
Rust is similar to Go in that it defines an error as anything
implementing a particular interface.
In Rust, that interface is three methods:
`Display` `fmt`, to print the display form of the error;
`Debug` `fmt`, to print the debug form of the error,
typically a dump of the data structure itself,
and `cause`, which returns the lower-level cause of this error,”
analogous to `Unwrap`.
Rust does not appear to provide analogues to the draft designs
`errors.Is` or `errors.As`, or any other helpers that walk the
error cause chain.
Of course, in Rust, the `cause` method is required, not optional.