go: proposal: errors: allow implementing err.Is(nil) == true
background
Currently you can implement, func (MyError) Is(target) bool { return true }, but MyError.Is will only be called if target, in errors.Is, is non-nil. This can be confusing to implementers of interface{ Is(error) bool }, and disallows signalling that a given returned error is nil. (This is useful when you want to return error metadata, but nothing actually failed.)
description
I propose removing the target == nil check from errors.Is. This will hand full control of equality checking to interface { Is(error) bool }, and make the user experience more consistent.
--- a/src/errors/wrap.go
+++ b/src/errors/wrap.go
@@ -37,10 +37,6 @@ func Unwrap(err error) error {
// then Is(MyError{}, os.ErrExist) returns true. See syscall.Errno.Is for
// an example in the standard library.
func Is(err, target error) bool {
- if target == nil {
- return err == target
- }
-
isComparable := reflectlite.TypeOf(target).Comparable()
for {
if isComparable && err == target {
costs
It is my understanding that we do not currently advocate for !errors.Is(err, nil) over err != nil, so the impact of this should be limited. Furthermore, if you have implemented func (MyError) Is(error) bool { return true }, you are either relying upon undefined behaviour or have a bug. That being said, this is a semantic change, so we should be careful to understand what the impact is.
alternatives
If the current implementation is preferred, it should be explicitly documented.Is reports whether any error in err's chain matches target, does not describe that for target == nil MyTarget.Isonly equality, and neverMyError.Is`, will checked.
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 2
- Comments: 23 (13 by maintainers)
Based on the conversation above, this seems like a likely decline. The weight of history weighs far too much against breaking all existing Go code in the world for a cleanup.
Hmmm. If linters are suggesting that code change
err == io.EOFtoerrors.Is(err, io.EOF), then I think those linters are making a mistake, and I hope they stop doing that. Similarly, it is absolutely fine when using golang.org/x/sys/unix to writeif fd, err := unix.Open(...); err == unix.ENOENT.I don’t think of
==anderrors.Isas being roughly the same thing. They are related, of course, but not the same. Using==lets you test whether an error is exactly some value. This is clearly useful and meaningful when usingsyscall.Errno, for example. Usingerrno.Islets you test whether an error contains some error, or more generally has some property. For example,errors.Is(syscall.EACCES, os.ErrPermission)returnstrue. This is so even though obviouslysyscall.EACCES != os.ErrPermission.For almost all code there is just one way to check errors:
err != nil. What we are talking about here is when we want to pick out a particular error property. And there isn’t just one way to do that. Sometimes we use==. Sometimes we use type assertions. Sometimes we useerrors.Is. Sometimes we useerrors.As. They all work in different cases. None of them are wrong.It’s true that we could say "everybody stop writing
err == io.EOFand start writingerrors.Is(err, io.EOF). It’s fairly likely that all code would continue to work if we made that change. But the Go compatibility rules mean that to the greatest extent possible,err == io.EOFmust continue to work in the future for cases where it works today. Anderr == io.EOFis perfectly clear. So there is no reason to for us to tell people to change it toerrors.Is(err, io.EOF). So we don’t do it.If people really want to write
errors.Is(err, nil), I won’t stop them. But lots of code writeserr != niltoday and in the future. We are never going to change that. We don’t want to change that. Given that fact, I think it would be unnecessarily confusing iferrors.Is(err, nil)were different fromerr == nil.That is not what we did. In the past, it only made sense to write
err == ErrXxxif the API of the function promised that it would returnErrXxxin some scenarios. And in the present and the future, that remains true: if the API of the function promises that it returnsErrXxxin some scenarios, then it is entirely reasonable and appropriate to writeerr == ErrXxx. There is no need to change any of the existing code, and it is perfectly fine to continue to write code that way.What we changed was to add a new capability: some functions may now explicitly define the API as returning an error that wraps some other error, typically be providing additional information. This isn’t going to be changed for code that already specifies that it returns
ErrXxx, of course. This specification is only going to be defined for APIs that return wrapping errors. For those APIs, you can now useerrors.Isto check for the error being wrapped. This intended to be convenient for cases where it is appropriate to wrap errors, such as uses ofos.PathError.The fact that we introduced
error.Isas useful for working with wrapping errors does not mean that we expect people to start usingerrors.Iseverywhere. We don’t expect that. Functions can continue to return specific errors, and those errors should normally be checked usingerr == ErrXxx, noterrors.Is. Nobody is expected to writeerrors.Is(err, io.EOF). They are expected to writeerr == io.EOF. Similarly for other explicitly exported errors, such asflag.ErrHelp.In other words,
err == ErrXxxis not an anti-pattern now, any more than it ever was. Usingerror.Isprovides a new facility for some functions. It doesn’t change how code is expected to work with errors that they return.@carnott-snap Today we have a world in which a function either returns an error (
err != nil) or does not return an error (err == nil). You seem to be envisioning a world in which a function either returns an error (!errors.Is(err, nil)) or does not return an error (errors.Is(err, nil)). If we had started with the second world, that would be a reasonable approach to consider. But today we are in the first world. Trying to change that today will give us a situation where nobody knows how to check whether a function returns an error, because they won’t know whether to writeerr != nilor!errors.Is(err, nil). What you suggest may well be very intuitive if we had done things that way from the start. But we didn’t. If we tried to change it now, the overall effect on the Go ecosystem would be, in my opinion, extremely confusing.No change in consensus, so declined.
You seem to be suggesting that we cannot migrate to away from equality checks. This is inaccurate for new apis. If I document that my api requires go1.13+ and returned
errors must be checked witherrors.Is, we do not need to worry about what==or!=do. This to me feels like what we have done witherrors.Is(err, ErrBad), assuming one implementinginterface { Is(error) bool },err == ErrBadwill not work. It confuses me that we are holdingnilas a special case, where we need to retain compatibility forerr == nil, but we are fine improving (and implicitly breaking) things forerr == ErrBad.Also, your world view seems to be missing a step:
err != nil) or does not return an error (err == nil).errors.Is(err, ErrXxx)) or does not return an error (err == nil).errors.Is(err, ErrXxx)) or does not return an error (errors.Is(err, nil)).Depending on how the compatibility guarantee is read, I would be understanding if we needed to deprecate
errors.Isand use a new name, likeerrors.Is2, for the few people who are relying uponerrors.Is(err, nil)today.