-
Notifications
You must be signed in to change notification settings - Fork 17.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
os: remove ErrTemporary in Go 1.13 #32463
Comments
I always thought it was a feature of What are other options for answering "is this error temporary"? You could define an interface and use
That is more cumbersome (although it does give you access to the concrete error if you want it). The only other option I can think of is to define an IsTemporary function, but now we have three ways of inspecting errors instead of two, and there is no easy way for new errors to mark themselves as temporary. |
I would expect a check for a general property to take exactly the form in @jba's comment above, using I would reserve |
Philosophically, now that errors can be composed, having errors that only exist to be composed with other errors doesn't seem bad to me. I don't see a difference between a sentinel meant to be wrapped by another error and something like Practically, var t interface { Temporary() bool }
if errors.As(err, &t) && t.Temporary() { and they ultimately say the same thing. It also makes it easy to do something like type invalidAccount struct{}
func (invalidAccount) Error() string { return "invalid account: permission denied" }
func (invalidAccount) IsPermission() bool { return true }
var errInvalidAccount invalidAccount (That example probably makes more sense for other kinds of sentinels-as-properties not defined in the stdlib but I just grabbed one of the ones that exists now. It's meant to be illustrative not definitive). |
Some more thoughts. The fact that Even so, I have a hard time arguing that specific errors refine an abstract “temporary” error. To me, that would be like saying that “square” refines an abstract “equilateral” shape. |
The standard library (mostly the If the question is whether If the question is whether As @jimmyfrasche says, from a purely practicaly standpoint, A small amount of library support can also make sentinels-as-properties quite easy to work with. err := properr.IsAlso(
errors.New("something took too long"),
os.ErrTemporary, os.ErrTimeout,
) |
Come to think of it, there actually is a significant difference between The On the other hand, That seems to imply that under the |
That same criticism applies to |
Wrapping an error (where "wrapping" means returning it from an In the very unusual case where you want to retain some, but not all, of the properties of an underlying error, this should be achievable by not returning the underlying error from Practically speaking, the standard library contains a number of places where error types go to some pains to forward |
Russ's point is that while you could imagine returning My opinion, in case it wasn't clear from my previous comment, is that we should live with that oddness for pragmatic reasons. Every other way of testing errors for properties is worse. We already have |
It's not burdensome, but it is subtle and easy to miss: retaining the properties of the wrapped error within As a thought-experiment, consider a unifying form: // Find calls found on each successive error in the chain of err
// until either found returns true or err is nil.
// The return value from Find reports whether found returned true.
func Find(err error, found func(error) bool) bool
Then we have three possible definitions for an func IsTemporary(err error) bool {
return errors.Is(err, os.errTemporary)
} func IsTemporary(err error) bool {
var temp interface{ Temporary() bool }
return errors.As(err, &temp) && temp.Temporary()
} func IsTemporary(err error) bool {
return errors.Find(err, func(err error) bool {
temp, ok := err.(interface{ Temporary() bool })
return ok && temp.Temporary()
})
} One might naively expect these three versions, using
|
It would be far less burdensome if the if target == os.ErrTemporary {
return false
}
return errors.Is(err, target) That would place the small burden (admittedly, on a possibly larger set of people) of calling |
The way to make the result of an If some, but not all, of the properties of an underlying error should be preserved, then the func (e *NeverGreen) Is(target error) bool {
if target == ErrGreen {
// This error is never green.
return false
}
// Otherwise, it is the wrapped error.
return errors.Is(e.err, target)
} |
@jimmyfrasche, that's true, but there would still be an inconsistency between The inconsistency between I'm honestly more concerned about the difference between |
This isn't a workaround. The reason to have an I don't think I'm following your point about embedding at all. A concrete example would be useful. |
|
The problem here is not the difference between The problem is your definition of what it means to be a temporary error. If we want to mark properties by types instead of sentinels, we need to use marker interfaces, like the one I wrote above:
There is no boolean return value. That resolves the asymmetry, and it also lets you recover the concrete value that is the temporary error.
Yes, you have to take special action to mask temporariness. But in your design, you have to take special action to propagate it, and that is the more common case. A temporary error will make its way up the call chain, being annotated with context. Only when it gets to the retry loop* will its temporariness be checked, and when that loop fails we're in a new regime: we've exhausted the programmatic actions that "temporary" implies, and it's time for something else. The author of the retry loop will have to recognize that fact when they return the new "retry failed" error. That error should not have an *I disagree with you all about the vagueness of "temporary." I think it means "this error may resolve itself in time without any action on your part," so the right response is to retry with backoff. Though admittedly there are edge cases—if you're out of quota and you won't get more for an hour, do you really want to retry? |
Decided to remove for Go 1.13. Long story below. net.Error was added with Timeout and Temporary in That first CL set the precedent for all Timeouts being Temporary.
This let us change this comment:
into the current one:
At the time, SetTimeout took a duration, so timeouts were temporary. The CL description calls out EMFILE:
If EMFILE is our canonical example of a temporary error, that seems fine. One option, discussed with @jba, @neild, and @mpvl, is that we could take So let's take ErrTemporary out for Go 1.13 and then try to put it back This would mean that people switching from err.(net.OpError).Temporary |
To be clear, the reason to remove ErrTemporary is that the definition is wrong and we have an opportunity to get it right. Being able to check for properties of errors seems like it could be useful enough to support this way, so that's less of a problem than the general confusion about what temporary means. |
A bit of a tangent but I think this is worth calling out. They're identical only if you consider that the Consider gRPC error codes, for example. You can't use
Then you'd do |
Updated title based on @rsc's comments above. |
The meaning of .Timeout() is also slated to be fixed in 1.13 #31449 |
I'm not yet sure what the outcome of #31449 (which is about
I'm afraid that if we add |
The only two useful semantics that I can think of are: Temporary—has a chance of succeeding if retried immediately or after a time span, without other changes; and Timeout—a deadline was exceeded, but the connection is intact, you can reset the deadline and try again. Everything else might be conceptually similar, but can't be handled in the same way, so shouldn't be bundled with these classes which have clear uniform resolutions. Otherwise, there's no point in putting these errors in the stdlib to be programmatically detected. (As further proof that |
Change https://golang.org/cl/188398 mentions this issue: |
The discussion with @rogpeppe on #32405 (specifically, #32405 (comment)) drives home how weird it is to use errors.Is to check for a cross-cutting error property as opposed to checking for an error kind. It requires the introduction of a variable of type error to pass to errors.Is, but that error is not something you'd ever want to return from a function as a description of what went wrong. You'd never say just "a temporary problem happened". You'd want to explain the specific problem; it may be true that some errors are temporary, but temporary is not what the error is.
ErrTimeout was introduced alongside ErrTemporary but that one does pass the smell test to me: a function might completely reasonably return ErrTimeout to say "the problem is that the operation timed out".
I'm leaning toward removing ErrTemporary for Go 1.13. Thoughts?
(A further complication is that I remain unconvinced that "Temporary" is even well defined as a concept—I cannot actually explain precisely what it means for an error to be temporary—but my comments above apply even if we assume it is well-defined as a property. The problem is that errors aren't properties, and so errors.Is probably isn't appropriate for testing properties.)
/cc @jba @mpvl @neild
The text was updated successfully, but these errors were encountered: