blob: b1bf05d4dd44309187dc3b8ea159cb3d595768b5 [file] [log] [blame] [view]
# Error Inspection — Draft Design
Jonathan Amsterdam\
Damien Neil\
August 27, 2018
## Abstract
We present a draft design that adds support for
programmatic error handling to the standard errors package.
The design adds an interface to standardize the
common practice of wrapping errors.
It then adds a pair of helper functions in [package `errors`](https://golang.org/pkg/errors),
one for matching sentinel errors and one for matching errors by type.
For more context, see the [error values problem overview](go2draft-error-values-overview.md).
## Background
Go promotes the idea that [errors are values](https://blog.golang.org/errors-are-values)
and can be handled via ordinary programming.
One part of handling errors is extracting information from them,
so that the program can make decisions and take action.
(The control-flow aspects of error handling are [a separate topic](go2draft-error-handling-overview.md),
as is [formatting errors for people to read](go2draft-error-values-overview.md).)
Go programmers have two main techniques for providing information in errors.
If the intent is only to describe a unique condition with no additional data,
a variable of type error suffices, like this one from the [`io` package](https://golang.org/pkg/io).
var ErrUnexpectedEOF = errors.New("unexpected EOF")
Programs can act on such _sentinel errors_ by a simple comparison:
if err == io.ErrUnexpectedEOF { ... }
To provide more information, the programmer can define a new type
that implements the `error` interface.
For example, `os.PathError` is a struct that includes a pathname.
Programs can extract information from these errors by using type assertions:
if pe, ok := err.(*os.PathError); ok { ... pe.Path ... }
Although a great deal of successful Go software has been written
over the past decade with these two techniques,
their weakness is the inability to handle the addition of new information to existing errors.
The Go standard library offers only one tool for this, `fmt.Errorf`,
which can be used to add textual information to an error:
if err != nil {
return fmt.Errorf("loading config: %v", err)
}
But this reduces the underlying error to a string,
easily read by people but not by programs.
The natural way to add information while preserving
the underlying error is to _wrap_ it in another error.
The standard library already does this;
for example, `os.PathError` has an `Err` field that contains the underlying error.
A variety of packages outside the standard library generalize this idea,
providing functions to wrap errors while adding information.
(See the references below for a partial list.)
We expect that wrapping will become more common if Go adopts
the suggested [new error-handling control flow features](go2draft-error-handling-overview.md)
to make it more convenient.
Wrapping an error preserves its information, but at a cost.
If a sentinel error is wrapped, then a program cannot check
for the sentinel by a simple equality comparison.
And if an error of some type T is wrapped (presumably in an error of a different type),
then type-asserting the result to T will fail.
If we encourage wrapping, we must also support alternatives to the
two main techniques that a program can use to act on errors,
equality checks and type assertions.
## Goals
Our goal is to provide a common framework so that programs can treat errors
from different packages uniformly.
We do not wish to replace the existing error-wrapping packages.
We do want to make it easier and less error-prone for programs to act on errors,
regardless of which package originated the error or how it was augmented
on the way back to the caller.
And of course we want to preserve the correctness
of existing code and the ability for any package to declare a type that is an error.
Our design focuses on retrieving information from errors.
We don’t want to constrain how errors are constructed or wrapped,
nor must we in order to achieve our goal of simple and uniform error handling by programs.
## Design
### The Unwrap Method
The first part of the design is to add a standard, optional interface implemented by
errors that wrap other errors:
package errors
// A Wrapper is an error implementation
// wrapping context around another error.
type Wrapper interface {
// Unwrap returns the next error in the error chain.
// If there is no next error, Unwrap returns nil.
Unwrap() error
}
Programs can inspect the chain of wrapped errors
by using a type assertion to check for the `Unwrap` method
and then calling it.
The design does not add `Unwrap` to the `error` interface itself:
not all errors wrap another error,
and we cannot invalidate
existing error implementations.
### The Is and As Functions
Wrapping errors breaks the two common patterns for acting on errors,
equality comparison and type assertion.
To reestablish those operations,
the second part of the design adds two new functions: `errors.Is`, which searches
the error chain for a specific error value, and `errors.As`, which searches
the chain for a specific type of error.
The `errors.Is` function is used instead of a direct equality check:
// instead of err == io.ErrUnexpectedEOF
if errors.Is(err, io.ErrUnexpectedEOF) { ... }
It follows the wrapping chain, looking for a target error:
func Is(err, target error) bool {
for {
if err == target {
return true
}
wrapper, ok := err.(Wrapper)
if !ok {
return false
}
err = wrapper.Unwrap()
if err == nil {
return false
}
}
}
The `errors.As` function is used instead of a type assertion:
// instead of pe, ok := err.(*os.PathError)
if pe, ok := errors.As(*os.PathError)(err); ok { ... pe.Path ... }
Here we are assuming the use of the [contracts draft design](go2draft-generics-overview.md)
to make `errors.As` explicitly polymorphic:
func As(type E)(err error) (e E, ok bool) {
for {
if e, ok := err.(E); ok {
return e, true
}
wrapper, ok := err.(Wrapper)
if !ok {
return e, false
}
err = wrapper.Unwrap()
if err == nil {
return e, false
}
}
}
If Go 2 does not choose to adopt polymorphism or if we need a function
to use in the interim, we could write a temporary helper:
// instead of pe, ok := err.(*os.PathError)
var pe *os.PathError
if errors.AsValue(&pe, err) { ... pe.Path ... }
It would be easy to mechanically convert this code to the polymorphic `errors.As`.
## Discussion
The most important constraint on the design
is that no existing code should break.
We intend that all the existing code in the standard library
will continue to return unwrapped errors,
so that equality and type assertion will behave exactly as before.
Replacing equality checks with `errors.Is` and type assertions with `errors.As`
will not change the meaning of existing programs that do not wrap errors,
and it will future-proof programs against wrapping,
so programmers can start using these two functions as soon as they are available.
We emphasize that the goal of these functions,
and the `errors.Wrapper` interface in particular, is to support _programs_, not _people_.
With that in mind, we offer two guidelines:
1. If your error type’s only purpose is to wrap other errors
with additional diagnostic information,
like text strings and code location, then don’t export it.
That way, callers of `As` outside your package
won’t be able to retrieve it.
However, you should provide an `Unwrap` method that returns the wrapped error,
so that `Is` and `As` can walk past your annotations
to the actionable errors that may lie underneath.
2. If you want programs to act on your error type but not any errors
you’ve wrapped, then export your type and do _not_ implement `Unwrap`.
You can still expose the information of underlying errors to people
by implementing the `Formatter` interface described
in the [error printing draft design](go2draft-error-values-overview.md).
As an example of the second guideline, consider a configuration package
that happens to read JSON files using the `encoding/json` package.
Malformed JSON files will result in a `json.SyntaxError`.
The package defines its own error type, `ConfigurationError`,
to wrap underlying errors.
If `ConfigurationError` provides an `Unwrap` method,
then callers of `As` will be able to discover the `json.SyntaxError` underneath.
If the use of a JSON is an implementation detail that the package wishes to hide,
`ConfigurationError` should still implement `Formatter`,
to allow multi-line formatting including the JSON error,
but it should not implement `Unwrap`, to hide the use of JSON from programmatic inspection.
We recognize that there are situations that `Is` and `As` don’t handle well.
Sometimes callers want to perform multiple checks against the same error,
like comparing against more than one sentinel value.
Although these can be handled by multiple calls to `Is` or `As`,
each call walks the chain separately, which could be wasteful.
Sometimes, a package will provide a function to retrieve information from an unexported error type,
as in this [old version of gRPC's status.Code function](https://github.com/grpc/grpc-go/blob/f4b523765c542aa30ca9cdb657419b2ed4c89872/status/status.go#L172).
`Is` and `As` cannot help here at all.
For cases like these,
programs can traverse the error chain directly.
## Alternative Design Choices
Some error packages intend for programs to act on a single error (the "Cause")
extracted from the chain of wrapped errors.
We feel that a single error is too limited a view into the error chain.
More than one error might be worth examining.
The `errors.As` function can select any error from the chain;
two calls with different types can return two different errors.
For instance, a program could both ask whether an error is a `PathError`
and also ask whether it is a permission error.
We chose `Unwrap` instead of `Cause` as the name for the unwrapping method
because different existing packages disagree on the meaning of `Cause`.
A new method will allow existing packages to converge.
Also, we’ve noticed that having both a `Cause` function and a `Cause` method
that do different things tends to confuse people.
We considered allowing errors to implement optional `Is` and `As` methods
to allow overriding the default checks in `errors.Is` and `errors.As`.
We omitted them from the draft design for simplicity.
For the same reason, we decided against a design that provided a tree of underlying errors,
despite its use in one prominent error package
([github.com/hashicorp/errwrap](https://godoc.org/github.com/hashicorp/errwrap)).
We also decided against explicit error hierarchies,
as in https://github.com/spacemonkeygo/errors.
The `errors.As` function’s ability to retrieve errors of more than one type
from the chain provides similar functionality:
if you want every `InvalidRowKey` error to be a `DatabaseError`, include both in the chain.
## References
We were influenced by several of the existing error-handling packages, notably:
- [github.com/pkg/errors](https://godoc.org/github.com/pkg/errors)
- [gopkg.in/errgo.v2](https://godoc.org/gopkg.in/errgo.v2)
- [github.com/hashicorp/errwrap](https://godoc.org/github.com/hashicorp/errwrap)
- [upspin.io/errors](https://commandcenter.blogspot.com/2017/12/error-handling-in-upspin.html)
- [github.com/spacemonkeygo/errors](https://godoc.org/github.com/spacemonkeygo/errors)
Some of these package would only need to add an `Unwrap` method to their wrapping error types to be compatible with this design.
We also want to acknowledge Go proposals similar to ours:
- [golang.org/issue/27020](https://golang.org/issue/27020) — add a standard Causer interface for Go 2 errors
- [golang.org/issue/25675](https://golang.org/issue/25675) — adopt Cause and Wrap from github.com/pkg/errors