golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
123.73k stars 17.62k forks source link

proposal: Go 2 error values #29934

Closed jba closed 2 years ago

jba commented 5 years ago

This issue is for discussion of our Go 2 error values proposal, which is based on our draft designs for error information and formatting.

This proposal will follow the process outlined in the "Go 2, here we come" blog post: we will have everything ready to go and checked in at the start of the Go 1.13 cycle (Feb 1), we will spend the next three months using those features and soliciting feedback based on actual usage, and then at the start of the release freeze (May 1), we will make the "launch decision" about whether to include the work in Go 1.13 or not.

Update 2019-05-06: See launch decision here: https://github.com/golang/go/issues/29934#issuecomment-489682919

mvdan commented 5 years ago

Assuming this is accepted and implemented before generics, would the design be adapted in a potential redesign of std with generics?

mpvl commented 5 years ago

@mvdan We think the design not using generics resulted in a slightly nicer API in the end. So we will probably not do so. That said, it can't be ruled out there will be additional API using generics.

networkimprov commented 5 years ago

Errors constructed with errors.New and fmt.Errorf will display differently with %+v

Could a change in output break consumers which compare it with a hard-coded string, e.g. test code?

neild commented 5 years ago

Errors constructed with errors.New and fmt.Errorf will display differently with %+v

Could a change in output break consumers which compare it with a hard-coded string, e.g. test code?

Conceivable, but since the fmt package currently treats %+v identically to %v for any type which implements the error interface there isn't a strong reason for users to use the former to format errors at the moment.

Sherlock-Holo commented 5 years ago

I wonder why xerrors.Errorf("new error: %w", baseErr) %w must at the end, I think when print the error, it should be

baseErr: some error

not

some error: baseErr

networkimprov commented 5 years ago

Agreed it's odd that the format string is an output-type controller, rather than a flag in the verb like %$v.

Also, have the authors considered providing this, which is conspicuously absent in package fmt:

func Error(a ...interface{}) error

This works like errors.New(fmt.Sprint(a, b)) Just as Errorf works like errors.New(fmt.Sprintf(s, a, b))

neild commented 5 years ago

Agreed it's odd that the format string is an output-type controller, rather than a flag in the verb like %$v.

What are the arguments for a flag over a verb? I don't see a strong argument for one over the other and %w has some mnemonic value, but perhaps I'm missing something.

func Error(a ...interface{}) error

I think this is orthogonal to the rest of the proposal, but it doesn't seem unreasonable. On the other hand, I don't think I've ever felt the absence of this function.

networkimprov commented 5 years ago

I'm referring to this output-type control via the format string: "if ... the format string ends with : %s, : %v, or : %w, then the returned error will implement FormatError." Why not a flag for %s & %v?

The easiest way to print an error is: fmt.Println("this happened: ", err) The easiest way to make an error should be: fmt.Error("this happened: ", err)

jba commented 5 years ago

Why not a flag for %s & %v?

The only existing flags that don't have a meaning for %s and %v are space and 0. But those both feel wrong, so we'd need to make up a new flag. That means one could potentially use that flag with any verb, but it would be meaningless except for %s and %v. That feels wasteful—flags and verbs should combine orthogonally to (almost) always produce a useful result (with space and 0 themselves being the notable exceptions).

ericlagergren commented 5 years ago

To be clear: does this mean each call to fmt.Errorf will collect a stack trace?

neild commented 5 years ago

To be clear: does this mean each call to fmt.Errorf will collect a stack trace?

A single stack frame, not a full trace.

tandr commented 5 years ago

To be clear: does this mean each call to fmt.Errorf will collect a stack trace?

A single stack frame, not a full trace.

Would it be possible to make it "adjustable" somehow? In our home-made logging wrapper we have noticed that we have to pull a couple or 3 frames to get to "the caller of interest", so printout looks more or less pointing to the place where logwrap.Print(err) function was called. If this is becoming a part of the standard, in case when wrapper (or utility method) is created to simplify an error creation, I would like to report the place where that method was called from, and not the utility method itself.

We did it by (arguable not so elegant) way as adding a parameter "how big is the stack offset" to the logging function, but I am not sure if that's the only option here. Adding special format symbol for stack, with number of frames interesting might be one of possible approaches.

neild commented 5 years ago

The intended usage is that additional frames be added as necessary by annotating or wrapping the error, possibly with additional context.

For example,

func inner() error { return errors.New("inner error") }
func outer() error { return fmt.Errorf("outer error: %w", inner()) }

fmt.Fprintf("%+v", outer())
// outer error:
//     /path/to/file.go:123
//   - inner error:
//     /path/to/file.go:122

To attach information about where a utility function was called from, you do so in the same way as today: By annotating the error with additional information.

We considered capturing a full stack rather than a single frame, but think the single-frame approach has some advantages: It's light-weight enough to be feasible to do for every error and it deals well with error flows that pass between goroutines.

That said, this proposal makes it easy for error implementations to add whatever detail information they want in a compatible way, be it full stacks, offset stack frames, or something else.

networkimprov commented 5 years ago

[the flag] would be meaningless except for %s and %v. That feels wasteful—flags and verbs should combine orthogonally to (almost) always produce a useful result (with space and 0 themselves being the notable exceptions).

Erm, I think you've refuted your own argument by raising exceptions :-)

: %s is magical, invisible, and readily broken without compiler complaint. And no other format string has similar effect. That's not Goish to my eye.

neild commented 5 years ago

: %s is magical, invisible, and readily broken without compiler complaint. And no other format string has similar effect. That's not Goish to my eye.

I'm not certain if you're making an case about verbs vs. flags (%v/%w vs. %v/%$v) or about using fmt.Errorf to annotate/wrap errors in general. Could you clarify your understanding of the proposal and what you're suggesting instead?

networkimprov commented 5 years ago

As I said above, I'm referring to this output-type control in the format string:

if ... the format string ends with : %s, : %v, or : %w, then the returned error will implement FormatError.

The control should be a flag e.g. %$s & %$v, and not the contents of the format string.

neild commented 5 years ago

The control should be a flag e.g. %$s & %$v, and not the contents of the format string.

Thanks; I think I understand you now.

The current design of applying special-case handling in fmt.Errorf to a : %v et al. suffix is informed by a couple of factors.

Firstly, we want to permit existing code to take as much advantage of the new error formatting features as possible. So, for example, errors.New and fmt.Errorf now capture the caller's stack frame by and print this information in detail mode. Existing code that creates errors does not need to change to include this information. But what about annotation of errors? Consider the following:

func f1() error { return errors.New("some error") }
func f2() error { return fmt.Errorf("f2: %v", f1()) }

func main() {
  fmt.Printf("%+v", f2())
}

We want this to print:

some error:
    main.go:1
  - f2:
    main.go:2

But if annotation is only enabled with a special format directive like %$v, then this will instead print:

some error: f2
    main.go:2

The innermost stack frame is lost.

The other consideration is that error annotation is linear. The errors.Formatter interface allows an error to format itself and return the next error to format. By design, it does not provide a way for an error to interleave its output with that of another error. This limitation makes the formatted output more consistent and predictable, but it means that there is no way to insert a hypothetical %$v into the middle of a format string:

// What would this error's `FormatError` method do?
return fmt.Errorf("some information about error: %$v (with some more information)", err)

These two considerations led to the current design of automatically annotating errors produced by fmt.Errorf calls that follow the most common pattern of adding information to an error, but only when the original error appears at the end of the string.

networkimprov commented 5 years ago

Re the stack frame, annotation could be enabled by any use of an error argument with Errorf(), instead of a magical, brittle format string.

there is no way to insert a hypothetical %$v into the middle of a format string

Would a format string "abc %v 123" work as expected given annotation enablement on use of an error argument? If not, I believe you need to rethink this...

jba commented 5 years ago

Would a format string "abc %v 123" work as expected given annotation enablement on use of an error argument?

No; we will only treat an error specially if the format string ends with ": %s" or ": %v".

If not, I believe you need to rethink this...

Can you explain why? Our goal with this feature is to instantly and painlessly bring the new formatting into most existing Go code. We'd rethink that if, for example, it turned out that most existing calls to fmt.Errorf that included an error did not put the error at the end with ": %s" or ": %v". If that is true, we'd like to know.

As Damien pointed out, if you do like to put errors in the middle of your format strings, you're going to have bigger problems with our proposal than its fmt.Errorf behavior. Even if you roll your own formatting with the FormatError method, you're going to have a hard time getting wrapped errors to display in the middle.

One more point: while we do expect people to continue to write fmt.Errorf calls that wrap errors, we also encourage custom error-creating functions that build specific errors and might incorporate other features, like frame depth (as in @tandr's request). E.g.

type MyError ...
func MyErrorf(wrapped error, frameDepth int, format string, args ...interface{}) *MyError
tandr commented 5 years ago

No; we will only treat an error specially if the format string ends with ": %s" or ": %v".

My concern here would be that a) not every message fits this format, and b) not every message is going to be in English.

"Something happened: This is What" is an ok way to produce a short human-readable error, but... Even in English, if I would want some text explaining "What to do now?" (remediation suggestion, "contact support" etc), that : %v might not be the last item in the formatting string. And the whole "at the end" logic will be completely broken for languages that are using Right-to-Left writing order (Arabic, Hebrew, Farsi, Urdu etc.)

Would it be easier to introduce a special formatter "print this as 'error'" and a modifier to make it "print this as an error with a stack"? I think there are enough letters in English alphabet left to cover one more case.

Also, if I may ask - no magic, please.

glibsm commented 5 years ago

The Unwrap function is a convenience for calling the Unwrap method if one exists.

// Unwrap returns the result of calling the Unwrap method on err, if err implements Unwrap.
// Otherwise, Unwrap returns nil.
func Unwrap(err error) error

It would also be nice to have a standard helper that can get to the root cause, i.e. the last error in the chain.

Similar to https://godoc.org/github.com/pkg/errors#hdr-Retrieving_the_cause_of_an_error.

neild commented 5 years ago

There are two points here, and it's worth considering them independently. The first is the manner in which error annotations compose.

The proposal adds a new errors.Formatter interface which errors may optionally implement. This interface provides two main pieces of functionality: Error detail in which an error may provide additional information when formatted with the %+v directive, and wrapping in which one error may add additional information to another.

A primary goal of the errors.Formatter interface is to allow different error implementations to interoperate. You should be able to create an error with errors.New, add some information to it with "github.com/pkg/errors".Wrap, add more information to that with "gopkg.in/errgo.v2/errors".Wrap, and get reasonable behavior.

Detail formatting of an error (%+v) produces multi-line output, with each step in the error chain represented as a section of one or more lines. For example:

could not adumbrate elephant:  - these three lines are the outermost error
    (some additional detail)   |
    labrynth.go:1986           /
  - out of peanuts             - and these two lines are the innermost error
    elephant.go:15             / 

Displaying a chain of errors in a consistent fashion requires making certain decisions about formatting: Do we print errors from the outermost to innermost, or vice-versa? How do we indicate boundaries between errors? If we leave these formatting decisions up to individual error implementations, we are likely to have situations where, for example, part of an error chain is printed inner-to-outer and another part is outer-to-inner; very confusing and not a good user experience.

To avoid confusion and simplify the work required to write a custom error type, we put the formatting of the error chain under the control of the error printer. Errors provide three pieces of information: The error text, detail text, and the next error in the chain. The printer does the rest. So, for example, the outermost error in the above example might have an ErrorFormatter implementation like:

func (e errType) FormatError(p Printer) (next error) {
  p.Print("could not adumbrate elephant") // error text
  if p.Detail() {
    p.Print("(some additional detail)") // error text only visible in detail (%+v)
    p.frame.FormatError(p) // print stack frame as detail
  }
  return e.wrappedErr // return the next error in the chain
}

Note again that an important property of this design is that an error doesn't choose how to format the error that it wraps; it returns that error to the printer, which then determines how to format the error chain. Not only does this simplify the error implementation and ensure consistency, but this property is important for localization of errors in RTL languages (as mentioned by @tandr), because it permits a localizing printer to adjust the output order as appropriate. (Most users will print errors using the fmt package, but note that this design deliberately allows for other implementations of errors.Printer.)

Given the above, the question then is how to make it simple for users to produce errors that satisfy these interfaces, which is where we get to the special handling of a suffix : %v. It will be helpful if critiques of this proposal are clear about whether a comment is about the above formatting interfaces, or fmt.Errorf specifically.

neild commented 5 years ago

It would also be nice to have a standard helper that can get to the root cause, i.e. the last error in the chain.

The equivalent in this proposal is the errors.Is function:

if errors.Is(err, io.EOF) { ... }

We believe this has several nice properties over a function returning the last element in a chain:

velovix commented 5 years ago

I like the motivation of the new Errorf behavior being a way to grandfather in old code. Near as I can tell, it could only make old code better. However, I'm less into the idea of Errorf being the main way to wrap errors. I could see programmers writing some error handling logic and, due to force of habit, accidentally writing fmt.Errorf("context: %v", err) instead of fmt.Errorf("context: %w", err) and missing out on potentially valuable information or effecting the intended error handling path. Since errors often find themselves in uncommon circumstances, it's possible to miss problems like these until an error in production occurs and you have less diagnostic information than you were hoping. For that reason, I think having a stronger, more compiler friendly wrapping solution available makes sense. Maybe something like github.com/pkg/errors.Wrap(err, message)?

To be a bit more concrete, I have more than once found myself in a similar situation with Python, where I raise an exception using f-strings but forget to put the "f".

raise RuntimeError("oh no! Something happened: {important_information}")

... instead of ...

raise RuntimeError(f"oh no! Something happened: {important_information}")

And since there's nothing syntactically wrong with what I did, I might not know until later when I need important_information that I've made a mistake.

networkimprov commented 5 years ago

Our goal with this feature is to instantly and painlessly bring the new formatting into most existing Go code.

Although that seems like a laudable goal, modifying the way existing code creates output is inherently risky. I suspect most of us would rather have an opt-in mechanism. Let project owners trial a feature and switch over (via go fix) when convenient for them.

IOW, new error-construction functions are needed, which always return errors.Formatter.

PieterD commented 5 years ago

While the fmt.Errorf changes to wrap the error is neat and conserves backwards compatibility with previous versions, a slightly nicer option going forward might be good?

When using pkg/errors, errors.Wrapf(err, "error reticulating spline %d", splineID) is the most used pattern by far. If that is also introduced, both of the most common patterns are maintained (aside from a switch from %v to %w in fmt.Errorf, which might cause trouble if one is missed.)

nhooyr commented 5 years ago

Wouldn't all the confusion go away if we just wrapped errors passed to fmt.Errorf by default? Instead of adding another verb. I can't see how this could cause any problems.

velovix commented 5 years ago

@nhooyr This was addressed in the proposal:

We decided against wrapping errors passed to fmt.Errorf by default, since doing so would effectively change the exposed surface of a package by revealing the types of the wrapped errors. Instead, we require that programmers opt in to wrapping by using the new formatting verb %w.

Meaning that users could suddenly unwrap your errors and start depending on the internal reason for the failure. This is bad because, as a library writer, it makes maintaining backwards compatibility more difficult.

nhooyr commented 5 years ago

Even if they unwrap the error, how would they be able to depend on an internal reason? Wouldn't it be an unexported type?

neild commented 5 years ago

Consider something like this:

func ReadMessage(r io.Reader) error {
  var b[1]
  if err := r.Read(b); err != nil {
    return fmt.Errorf("error reading length: %v", err)
  }
  if b[0] == 0 {
    return io.EOF // 0 length indicates end of data
  }
  // ...
}

If fmt.Errorf wraps by default, then the following will observe an io.EOF from the initial Read, which may not be intended:

if err := ReadMessage(r); errors.Is(err, io.EOF) {
}
neild commented 5 years ago

errors.Wrapf

I just wanted to note that this would need to be fmt.WrapErrorf or the like, because the fmt package depends on the errors package, not the other way around.

networkimprov commented 5 years ago

for package fmt

func Error(a ...interface{}) error                        // counterpart of F/S/Print()

func Errorw(e error, a ...interface{}) error              // wraps
func Errorwf(e error, f string, a ...interface{}) error

func FmtError(a ...interface{}) error                     // formats; FormatError return nil
func FmtErrorf(f string, a ...interface{}) error

func FmtErrorw(e error, a ...interface{}) error           // both; FormatError return e
func FmtErrorwf(e error, f string, a ...interface{}) error

Which has slight drawback of yielding fmt.FmtError(a, b).

nhooyr commented 5 years ago

If fmt.Errorf wraps by default, then the following will observe an io.EOF from the initial Read, which may not be intended:

I don't see how that could not be intended. If you're using errors.Is, you want to compare against one the wrapped errors. If you don't intend on doing that, don't use it. Given errors.Is is new functionality, wrapping all errors and making them available via unwrap is not a breaking change and thus should not change the functionality of existing programs. While yes, it does change the exposed API of existing libraries, it doesn't do so in a negative way that justifies having another format verb.

Also, can't you use Printer interface anyway to get the next error even if they didn't use %w? So the type is still exposed.

rsc commented 5 years ago

It's true that recognizing : %v is a bit magical. This is a good point to raise. If we were doing it from scratch, we would not do that. But an explicit goal here is to make as many existing programs automatically start working better, just like we did in the monotonic time changes. Sometimes that constrains us more than starting on a blank slate. On balance we believe that the automatic update is a big win and worth the magic. To the point about "sometimes people don't follow the convention and use Errorf("context: %v")", that's OK. Go encourages following convention and dispenses benefits to those who do (gofmt, interfaces, etc).

josharian commented 5 years ago

@rsc what are your thoughts about RTL languages? Should there also be magic prefix recognition?

flibustenet commented 5 years ago

Why not just %v at the end ? it's difficult to see the difference between :%v : %v :   %v... Maybe also accept trailing spaces after %v  

networkimprov commented 5 years ago

@rsc the issue here is not simply magical strings; it's error-case output generation. It is essential to let your customers decide how/when to upgrade that sort of code. EDIT: If "existing programs automatically start working better" due to altered error-case output, that will cause error-case tests to fail.

And I think you've overlooked the benefits dispensed by go fmt -r and go fix :-)

The fmt API I suggested above is one correct way to provision these features. Please provide a correct way, and then address how existing programs would upgrade to it.

Another reason to keep APIs clean is that far more code will be written in Go than has been written in it.

JavierZunzunegui commented 5 years ago

It would also be nice to have a standard helper that can get to the root cause, i.e. the last error in the chain.

The equivalent in this proposal is the errors.Is function: ...

  • Nicely analogous to errors.As, which converts an error to a type.

This overstates the capacities of errors.Is and errors.As. errors.Is only really works for sentinel errors, errors.As is a nightmare to use with un-exported error types. I show just how difficult it is to get such an error (errors.Opaque in my example) using errors.As in https://play.golang.org/p/IZgPf9f62it.

Also errors.As has a very dangerous api, the argument is meant to be a pointer to the type implementing error (**Err for func (*Err) Error() string {...} / *Err for func (Err) Error() string {...}). If you pass it a *error there will be no match, even if the type in the error interface is *Err / Err. I show this in https://play.golang.org/p/m4UsDdEkJzO. This will no doubt became a gotcha and cause many bugs if adopted.

Why not drop errors.As and go for the much simpler Last(err, func(error) bool) error that is perfectly understandable, safe and more flexible?

agnivade commented 5 years ago

I find the %w formatting verb to be like a hidden trick. To me, it is surprising that a formatting verb is doing something other than just specifying how something is going to be printed.

I understand that the objective is to allow existing code to take advantage of wrapping. But we are adding a new verb anyways, which needs users to modify their code. So why not have a new API to wrap errors which takes an error as a parameter ? That is simple and direct. The objective is immediately clear and there is less cognitive load to understand more concepts.

ericlagergren commented 5 years ago

Is there a reason why error properties were omitted from the design of the proposal? As @JavierZunzunegui mentioned, errors.Is and errors.As only work if you know the specific error constant/variable or concrete type ahead of time, respectively.

The proposal doesn’t allow you to ask questions of the error (chain) like, “Was it caused by a timeout?” and “Is it temporary?” which are quite useful. (See: net.Error and Upspin’s errors package.)

IMO, that omission kneecaps this new errors package in a way because in order to use the new package and check error properties we’ll either have to (AFAICT, kludgily) try and implement properly checking via the new Is interface, or—more likely—abandon it entirely for our own errors package.

nhooyr commented 5 years ago

The proposal doesn’t allow you to ask questions of the error (chain) like, “Was it caused by a timeout?” and “Is it temporary?” which are quite useful. (See: net.Error and Upspin’s errors package.)

That is possible with the API if you could pass a pointer to an interface to errors.As, e.g. net.Error and then errors.As would populate the first error that implements that interface into the value. Then you could do whatever check you want yourself. Not sure if this already works.

ericlagergren commented 5 years ago

@nhooyr I suppose it could, though right now it doesn’t: https://play.golang.org/p/FA6bRqmBf2A

It would be nice to have explicit support for error properties from the get go.

neild commented 5 years ago

This overstates the capacities of errors.Is and errors.As. errors.Is only really works for sentinel errors, errors.As is a nightmare to use with un-exported error types. I show just how difficult it is to get such an error (errors.Opaque in my example) using errors.As in https://play.golang.org/p/IZgPf9f62it.

I don't quite follow your example. The purpose of errors.Opaque is to hide an error from Is and As. The intended use of As is in cases such as:

var pathError *os.PathError
if xerrors.As(err, &pathError) {
  fmt.Println("Failed at path:", pathError.Path)
}

Why not drop errors.As and go for the much simpler Last(err, func(error) bool) error that is perfectly understandable, safe and more flexible?

I do think we need such a function, to deal with predicate functions such as os.IsNotExist if for no other reason.

neild commented 5 years ago

Is there a reason why error properties were omitted from the design of the proposal? As @JavierZunzunegui mentioned, errors.Is and errors.As only work if you know the specific error constant/variable or concrete type ahead of time, respectively.

There are a few ways to provide properties under this proposal.

As one example, consider this function:

// NewPredicate returns an error which matches any error for which f returns true.
func NewPredicate(text string, f func(error) bool) error { /* ... */ }

We can easily use this to produce tagged errors:

// ErrTemporary is a property that may be satisfied by many errors.
var ErrTemporary = errors.New("temporary error")

var ErrOne = NewPredicate("error one (temporary)", func(err error) bool {
  return err == ErrTemporary
})

We can check to see that errors.Is(ErrOne, ErrTemporary); this easily generalizes to any number of properties.

Or another possibility might be something like the following:

// Tag wraps err in an error that matches a set of tags.
func Tag(text string, errs ...error) { ... }

var ErrTwo = Tag("error two (temporary)", ErrTemporary)

The implementations of NewPredicate and Tag are quite simple under this proposal. (I'm appending an implementation of NewPredicate as a demonstration.) Since it is so simple for these features to be implemented outside the standard library, we felt it was best to leave the specifics to third-party packages to start with.

type predicateError struct {
        text  string
        f     func(error) bool
        frame xerrors.Frame
}

func NewPredicate(text string, f func(error) bool) error {
        return &predicateError{text, f, xerrors.Caller(1)}
}

func (e *predicateError) Error() string              { return e.text }
func (e *predicateError) Is(err error) bool          { return e.f(err) }
func (e *predicateError) Format(f fmt.State, c rune) { xerrors.FormatError(e, f, c) }

func (e *predicateError) FormatError(p xerrors.Printer) error {
        p.Print(e.text)
        if p.Detail() {
                e.frame.Format(p)
        }
        return nil
}
jimmyfrasche commented 5 years ago

One thing I do not see addressed in the proposal: will Unwrap methods be added to existing stdlib types that wrap an error, such as *os.PathError? I would assume so but I did not see it spelled out anywhere.


@neild

Why not drop errors.As and go for the much simpler Last(err, func(error) bool) error that is perfectly understandable, safe and more flexible?

I do think we need such a function, to deal with predicate functions such as os.IsNotExist if for no other reason.

Wouldn't os.IsNotExist be updated to work on wrapped errors?

ericlagergren commented 5 years ago

@neild That's what I meant by kludgy (wrt error properties).

For example, a super quick sketch of different scenarios: https://github.com/ericlagergren/errors-example

poll0 is an example of a current errors package (it borrows from Upspin's errors package). It's straightforward, requires only one import, and allows automatic translation between well-known external error properties and the set of project-specific error properties. Other simple functions like xerr.IsSentinel(err, sentinel error) bool and other functions can be added that traverse the chain to find sentinel errors.

poll1 is basically what we get if we try to graft in this new errors proposal with poll0. We still have to lean on our project-specific errors package, xerr, which means in order to do anything useful with errors we have to import two packages. We also rely heavily on the error variable implementing the errors package's hidden Is interface and checking for our project-specific error properties, which, admittedly, has both pros and cons. But, I don't see enough benefits to switch to it instead of xerr.Is.

poll2 is an example of interfacing with properties implemented via interface methods (like the well-known net.Error). IMO, it's the worst of all the scenarios.

poll3 is a little nicer than poll2, but doesn't work with the proposal's current implementation. (At least, not without the error variable implementing the hidden As interface.)

Given the four pollX options, I'd probably just ignore this new errors package and continue using poll0 (or some other existing errors package that provides property checking via methods).

Ideally, a Go 2 errors package would provide explicit support for one "blessed" form of error properties, just like it does sentinel errors.

dsnet commented 5 years ago

poll2 seems problematic since it assumes that err is not wrapped, so the assertion on L49-50 and L57-58 don't always hold true.

poll3 is a little nicer than poll2, but doesn't work with the proposal's current implementation.

Why is that?

Assuming that xerrors.As works, then poll3 can be further cleaned up as:

var reauth xerr.ReauthError
var retry xerr.TimeoutError
switch err := someAPIQuery(); {
case errors.As(err, &reauth) && reauth.Permission() && reauth.Temporary():
    ...
case errors.As(err, &retry) && retry.Timeout() && retry.Temporary():
    ...
default:
    ...
}

This is two lines longer than poll0. It also shows one advantage of the non-generic version of xerrors.As in that it allows you to obtain the concrete error value within a switch statement, while that wouldn't be possible with the generic version of As that returns the concrete error value.

ericlagergren commented 5 years ago

poll2 seems problematic ...

Yeah, that's a good catch and my mistake is another reason why it's the worst of the four options.

Personally, I don't mind poll3. But, at least right now, it doesn't work (see: https://play.golang.org/p/FA6bRqmBf2A) without implementing the hidden As interface.

As could be changed to allow pointers to interfaces to add explicit error properties support, though I didn't spend much time thinking about possible bugs:

func As(err error, target interface{}) bool {
    if target == nil {
        panic("errors: target cannot be nil")
    }
    typ := reflect.TypeOf(target)
    if typ.Kind() != reflect.Ptr {
        panic("errors: target must be a pointer")
    }
    targetType := typ.Elem()
    for {
                if errType := reflect.TypeOf(err); errType.AssignableTo(targetType) {
            reflect.ValueOf(target).Elem().Set(reflect.ValueOf(err))
            return true
        }
        if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
            return true
        }
        if err = Unwrap(err); err == nil {
            return false
        }
    }
}
dsnet commented 5 years ago

Ah I see, the issue is that matches in As is defined as exact type match.

Personally, I would argue that it should follow the definition of assignability according to the Go language, and provided by just calling the reflect.Type.AssignableTo. It seems entirely sensible that you would want to match against an interface.

I think assignability is what people have in mind in terms of how they expect As to function (or that's just me).

ericlagergren commented 5 years ago

@dsnet Yes, that would make integration much easier. And I think it's (at least similar to) what @\JavierZunzunegui was getting at with his Opaque example.