-
Notifications
You must be signed in to change notification settings - Fork 18k
errors: Is and As make it impossible to maintain error hygiene #32362
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
Comments
The generic You could even imagine adding a new well-known method (perhaps |
I don't believe it's entirely a red-herring. The type you're wanting to preserve might not be defined in the package that wants to preserve it. It's not uncommon for modules to use a common errors package shared by many packages, for example. Another example: I might want to preserve a database-specific error type. Pq isn't a great example because all the fields are exported, but that might not be the case in future or with other drivers. Also, AFAICS the flatten operation can need O(num-errors-in-chain) allocations if you want to preserve stack information, which isn't ideal. I guess there might be ways around that though. |
FWIW the same issue applies to
This satisfies both |
That's true, but I think that only reinforces my point: it is up to the author of the error (or error type) — not the user of the error — to decide how much information that type or value is capable of masking. |
If you want to preserve the first |
I think the problem is the errors.As/Is function will check for As/Is methods on the first error, but if it returns false, they will keep unwrapping the chain and checking until they find something that returns true. They should instead short-circuit the first time an error in the chain implements an As/Is method. |
If I understand your issue correctly, the more general form of what you ask is given I don't think there is a way of doing that in the errors package as it is currently proposed. It can be done trivially easy if doing error wrapping using a linked list of errors, but that won't be supported under the new changes. An example: JavierZunzunegui@4489292#diff-653dcb659c69a84ac6ce23e6e48509a3R6 |
I believe this is an implementation of
Proof: If direction: if U is identical to T, we skip past the initial Only if direction: we need to show that if U is not identical to T or if |
We considered that seriously for a while. Ultimately we rejected it. It does give the As/Is methods more power, but the cost is that every such method must follow the chain to get the right answer. We opted for the less powerful but easier to implement semantics. |
In my comment above I left out an important part of the proof: the I also mixed up the wording: I wrote about what |
Another way to think about this problem is that there are multiple uses for error chains:
The current Is/As functions tie the first two kinds of errors chains together, when conceptually they are different. |
Just call the function on your wrapped error if you want the default semantics. That’s not hard to implement. Or just don’t have a method at all and get default semantics for free. I think you only want the method at all because you want to do something unusual. |
KeepType needs to remember its Fleetwood Mac: never break the chain! |
I struggle to see a situation where you want non-transitive Is: The similar case with A concrete example would help here, I think. |
This seems odd to me. Why should the power of hiding errors be up to the creator of the error rather than the code that wants to hide it? There is an alternative to walking the chain. Each time we wrap an error, instead of recording a link in the chain, we instead decide whether to keep or drop the underlying error value. I don't actually believe that it's necessary to expose the concept of an error chain at all. Now that the printing and source location concerns have been removed from the errors package, it has become clear to me that all we really need here is a way of distinguishing error metadata from the actual error itself. By metadata, I mean extra information such as message annotation and source location info. One thing that was concerning to me about the current proposal is the amount of mechanism involved. Not only do we have an To me, this feels a bit wrong. Error handling is one of the lowest levels of the language. I would like an errors package that feels irreducibly simple, and the current proposal doesn't currently make me feel that way. How about something like this instead, as the entire implementation of the errors package?
We don't need the
Although the Once we have this, error annotation and other error metadata can be left to other packages. The Here's a simple error-wrapping example:
|
Just want to add a big +1 to this point in particular. I think the current design's reliance on reflect isn't a good idea, and the need for reflectlite to exist is a good example. |
Responding to a couple of previous questions:
Yes, that works. I'd forgotten about the As-method check. But, as I said above, I'm really not comfortable with the complexity of the current implementation. The With the trivial implementation I've suggested above, all the complexity melts away AFAICS.
Think about
I don't understand this. |
The intent of Unwrap/Is/As is to provide error information to programs, for handling (the second bullet). The The two cases correspond to two
|
It's unfortunate that we needed reflectlite, but that's an implementation detail. You need reflection for |
We address this in our draft design: "We feel that a single error is too limited a view into the error chain. More than one error might be worth examining."
So |
I find it difficult to buy the justification there. There's only one example in the design document (os.PathError), and os.PathError is almost exclusively used just for its error string annotation. The single counter-example in the standard library is arguably only there because There is nothing stopping you examining more than one error - any given error type can expose access to its underlying error; but I haven't yet seen any persuasive argument that all error inspection needs to traverse the chain. I think that this single aspect of the design accounts for almost all the complexity of the result, so it's worth more justification IMHO.
I'm not sure that the generality of Of the eight implementation of the If we want
Then in the
This would then cover the only other place that
I'm very sorry that I seem to have seriously derailed this from the original issue. All this really belongs in a separate issue, as it's clear that you can maintain error hygiene with the existing design, albeit with some awkwardness (this is how KeepType would actually need to look, I think). I'm just uncomfortable with the current design. If it lands now, we'll be stuck with it forever and I don't feel we've had enough experience so far with it to justify it. |
Go already has a type assertion statement. The fact that such a low level package as |
This example confuses me, because |
Yes, that's of course true. But if you're thinking of error chains, one could consider that |
I'm closing this issue in favour of #32405. |
It's typical that Go code has many lines of code that return errors as a consequence of other errors. In large programs, it can be hard to refactor this code with confidence because by changing the code, we might be changing the set of possible errors that can be returned, and some unknown caller might be broken by the refactor because they're relying on a specific error that's no longer returned.
The nature of such breakage is that it often goes unnoticed for long periods because errors are usually infrequent.
For this reason, I believe it's good to maintain what I call error hygiene: to restrict the set of returned errors to those that are explicitly part of a function's contract.
One good example of this is
fmt.Errorf("message: %v", err)
which hides the type and value of err from any subsequentIs
orAs
call.Unfortunately, I don't believe it's possible to preserve error hygiene in general with the scheme used for Is and As.
Suppose that we want to define the following function that we'll use to restrict error values to a particular type. This is a bare-minimum requirement for error hygiene.
How can we implement this function? For errors that don't fit the designated target type, the implementation is easy: just hide the whole error chain. However, if there's an error that does fit the target type, there's a problem: we can't preserve the target type without also preserving the error chain that's below it, because in general the chain is held in a field within the error value that we might not have access to. We can't make a new error value because then it wouldn't be the required target type.
I think that
As
andIs
are thus potentially problematic and we should consider whether it's a good idea to keep them as is. (so to speak :) )The text was updated successfully, but these errors were encountered: