-
Notifications
You must be signed in to change notification settings - Fork 17.9k
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
proposal: ErrorOr type wrapping (T, error) #51931
Comments
I think it's wroth pointing out, that this doesn't need an stdlib
Again, there is no need for this to live in the stdlib. Note that even today, it's not uncommon to just define a custom
I think an a little bit more instructive example is You treat this "exception" as bad, say that But really, that's besides the point. Because I think if anything
I think what you describe here is essentially the |
This doesn't look like it enables any shorter, safer, or clearer form of error handling, and instead just adds an extra layer of indirection. If done in the standard library, this would be a significant churn of what is considered idiomatic for what is arguably little benefit, and it can't even handle the common usecase of multiple return values, resulting in even more inconsistency. |
I agree. It doesn't need to be in the stdlib. The only reason I mentioned the stdlib is because there might be error-returning functions in the stdlib that would like to use
I agree with you but I think a little more needs to be said. The """(...) a Reader returning a non-zero number of bytes at the end of the input stream may return either err == EOF or err == nil.""" So, if we consider the following case:
This is equivalent to the But at the same time, if I think really long term, I imagine that at some point the At this point, the
Can you point me to the feedback for the try proposal so that I can evaluate whether the feedback applies to the
The following is shorter (less chars), safer and clearer (it's either error or value, no ambiguity): func (...) Error[int] { than: func (...) (int, error) { This is shorter and clearer (less variables): x := myfunc1() than: x, err := myfunc1() This is shorter (less args / return values) and clearer (API type shows it's error-agnostic): func (g *Group[K, V]) Do(key K, fn func()V) (v V, shared bool) than: func (g *Group[K, V]) Do(key Key, fn func()(V, error)) (v V, err error, shared bool) This is shorter and clearer: chan ErrorOr[int] than: type IntOrError {
v int
err error
}
chan IntOrError
The question of idiomacy is very debatable because the current error idiom was established in a pre-generics era. Now, with generics we want to rethink what the idiom should look like. That's the whole point of issues like this one and also #48287. If we don't allow the idiom to change ever, then we will never be able to use generics to their full potential. The whole point of a feature like generics is so that we can improve the language and its libraries across and reach a new level of idiom.
I think it can, e.g., Also, if Go ever supports tuples, we can also combine |
The error handling proposals have been focused on checking the error part, not the declarations. Saving a few characters on the function declaration is missing the point.
is arguably less clear, as without context (such as knowing the type), it doesn't look like there is an error path in here. I'm not saying that idioms can't change, but they have to bring benefits proportional to the cost they incur. Right now the benefits of this proposal aren't very clear. Types are written once, but the values they produce are used many times.
vs
Here, it's longer and harder to read, with more symbols in the way.
Feels like a step backwards, your type declaration is now much longer (and you haven't named the fields yet), and if you move the struct to be a named type, you now have to deal with the an inflation of types that exist solely to return multiple values when the language can already do that. You're also forcing return values to be grouped together when they may have no reason to be. |
A somewhat minor point, but I think of |
I did. There's probably more, on golang-nuts or the subreddit or the gopher slack, but TBQH I'm not really better at finding that than you and the github issue should be plenty. IMO it is pretty much out of the question that the same arguments apply. The only real difference between the two is that one spells it |
FWIW the true benefit of providing it as a third party package is that it makes this discussion completely obsolete. You could just write that package, publish it and publicize it and no one could object to that. If the community likes it, people will start using it. And instead of arguing about its value in the abstract, you could file a proposal in year or two pointing at all the projects using it, to make the case of moving it into the stdlib. I highly doubt that an addition like this would make it into the stdlib without that path. Even if we would, say, add the type to I think the history of It just makes things better for everyone involved - you don't have to argue that it's a good idea and just do it, the Go projects can benefit from the experience gained by the community over the time as a third-party package and when we finally do add it, we can discuss bigger changes. Maybe even to the language proper. Maybe allowing the existing stdlib functions adopt it, without breaking compatibility. |
The problem you are describing already exists in Go today and it relies on naming conventions (or context) for clarity. For example, the following examples are made clear not because we know the types of the variables but because we follow a naming convention. With x := f() // A value (i.e., not an error)
err := f() // An error
x, err := f() // A value and error
x, y := f() // 2 values
x, ok := f() // A value and a bool
x, y, ok := f() // 2 values and a bool
In the general case, we should assume that both the caller and callee use x := foo()
if x.Err() != nil {
return x
}
A lot of APIs don't name the return values either when using
The reason is so that we can instantiate generic types such as
I agree. If you have some ideas I'd love to hear more. Here are few ideas (in no particular order):
Does this go in the direction you're thinking? Feedback would be great! I agree with your approach. I will create a third party package with the A few more thoughts below:
I don't want to lose sight of the bigger picture here. This proposal is to overcome the problems with the titled sections "The 4 states of (T, error)", the "Error-agnostic generic APIs", the "Errors / values in channels, maps, slices", the "Naming problem with err", the "Inconsistent use of (T, error)". The section about "Future ideas" is out scope and is only meant as an idea. So I don't want to reduce the whole proposal to just the "Future ideas" section. I will take a closer look at the
The point of this discussion is to rethink error handling in the presence of generics. I think this got lost when my proposal's title was unilaterally changed to something with a different meaning. I have changed the title back and I'd prefer (if possible) that the title's meaning remain unchanged, although the actual wording can be changed. This proposal is not about defining the |
On the title, proposals should have a clear scope and design that can be evaluated and decided upon. If you want a general discussion of error handling possibilities, it's out of scope and should be done in one of the forums |
I followed @robpike 's suggestion to create a proposal and chose the title following his comment "You are asking for a new way to think about errors.". So I don't see what is wrong with that.
This has a clear scope described in the "Objectives" section and a clear design described in the "Design ideas" section. I would like to have a title along the lines of "thinking error handling in the presence of generics" without you unilaterally changing it without my consent. Please let me know how we can get there. I'm open to suggestions on wording / spelling if that's the blocking issue. |
FWIW I did respond to all sections as well. Except, I guess, this one, where I also thought of something else to say:
You are correct that there are 4 possible results. What I find significantly less clear is a) how much of a problem that is and b) how much of that problem your proposal solves. There are three potential problems I can see with this:
To me, this proposal seems to try and solve things which appear non-problems to me, while doing little to address the things which I perceive as problems. I would like at least some sort of data on how much of a problem the things it's trying to address are.
The scope, according to the "Objectives" section, is to introduce a new type. So, the title seems accurate. I agree with @seankhliao that a more general "rethinking error handling" is not an appropriate github issue or proposal and should be done in a place more conducive to open-ended discussion, like golang-nuts, reddit, slack, the twitter community or some other forum. I also think that you can be expected to abide by the processes set out by the Go project. The people who use github issues the most to track their day-to-day work should also have the strongest word in how they are used. There is nothing "unilateral" about that - if anything, your want for a specific title incompatible with that usage is unilateral. |
I'd be interested in hearing more about this. How would you go about addressing it statically?
Yes, you're right. I updated the "Objectives" section to better reflect what I intended.
I want to abide by the processes and AFAIK I am abiding by the processes. This is why I said I can change the title. That's not a problem. I reviewed the proposals which doesn't say anything about the title of the Github issue, so AFAICT my title should be fine (unless I missed something). I also asked for the title policy to be shared with me, but I only got that link to the proposals, so I'm assuming that's all there is to it. Unilateral in this case means that the title was changed by someone else without ever asking me to change the title, without giving me a chance to evaluate the policy (which I haven't seen still) and a choose a better title, without a explanation of why the title was changed, without a reasoning for the new title. That's what unilateral means. I expect mods to use go through a process of first asking the proposer to change the title (also good is to propose new titles) and if the proposer is clearly not abiding by the processes, then the mods can use their permissions to change the title. But that is not the case here. I have demonstrated willingness to work with the process and even shown openness to change the title. So this is clearly not the case of someone not abiding by the rules. So I don't think it's correct for the mods to change the title in this case because the problem has not escalated to a place that it requires action from the mods' side. I think that's a misuse of the mods' permissions. Also, at this point, the context is already layed out in the discussion and it's clear that the title "ErrorOr type wrapping (T, error)" is wrong. So I think it's harmful to purposefully change the proposal's title back to a title that has already been discussed that is harmful for this proposal. I don't think this is right. I would like to request once more that we can start a discussion to change the title to a new title that resembles more something like "rethinking error handling in the presence of generics". Are y'all willing to work with me to get us closer to that place? Or is this the end of the title discussion? |
I disagree. The name, as well as some details, might be up for debate, but ultimately, that's the concrete change you are proposing. And it's not at all uncommon to use placeholder names in proposal-titles, even if they are still up for debate. You seem to instead want to make this a non-proposal and just be an open-ended discussion about how generics might influence error handling. But you've been told that this is not how github issues are used in the Go project and that such a discussion should happen on one of the forums more suited to that. So, if you want that, you might want to close this issue and start a discussion there. Issue titles and labels are used by bots, searches and boards to triage and categorize issues, based on how they should be handled. These processes are not necessarily fully documented, relying instead on human judgement - generally, this is a good thing, as it allows more flexibility. Someone needs to make that judgement, and for that there is a set of people trusted to have enough of an understanding of the processes involved and triage issues accordingly. One such person has triaged this issue, to the best of their abilities. You might disagree with that triage, and there is some flexibility (e.g. if you prefer a different type name), but the basics of a) github issues are not for open-ended discussions, so the issue should focus on a concrete change and b) the title should reflect that change, won't budge. The question is, is this really what you want to spend your time arguing about? Trust me when I say that, for the concrete change you are proposing, "introduce |
Based on the discussion above, this proposal seems like a likely decline. |
No change in consensus, so declined. |
Hi everyone,
Following @robpike 's suggestion, I'd like to create a proposal for a new way to think about errors given that Go supports generics now.
This is similar in spirit the plan to think about APIs in the presence of generics but for error handling specifically.
Preliminary notes:
ErrorOr
,Val()
,Err()
, etc, are meant for illustration purposes only and any new names are welcome!Objective
Rethink Go's error handling mechanism in the presence of generics to overcome the following problems:
(T, error)
allows for 4 possible states but the majority of APIs only use 2 states and the other 2 states could be considered incorrect.T
and generic functions likefunc()T
cannot be instantiated with(T, error)
orfunc()(T, error)
respectively. This means API developers must artificially bake in errors in their APIs to allow callers to pass errors or pass error-returning functions.err
.(T, error)
The following are out of scope for this proposal:
context.Context
problem for APIs.(x, y, z, ...)
argument like a tuple.(T, error)
return like a tuple (see alternatives considered).Background
The 4 states of
(T, error)
The current mechanism to return errors is
(T, error)
which being a product type means it allows for 4 possible states, i.e., any combination of a proper / improper value, and an error ornil
. But the majority of APIs only care about either returning a value or an error, they don't care about the other 2 states, which could even be considered incorrect. Notable exceptions to this rule are theio.Reader.Read
API (more on this later).Because most APIs care only about 2 states, there is an opportunity to leverage the type system and generics to eliminate the 2 undesirable states, by introducing a new type
ErrorOr
that can only represent a value or an error, and doesn't allow for any other states. This would be used in new generics aware APIs.Error-agnostic generic APIs
One such API is the
singleflight.Do
, which could be reimagined with generics as:The
Do
API is error-agnostic, i.e., in principle it could accept infn
either an error-returning function or not because it doesn't do anything with the error. But in current generics, it's not possible to pass an error-returning function tofunc()V
sinceV
cannot be instantiated with multiple-return values or(T, error)
.This applies not only to
singleflight
but to any API that accepts a generic argumentT
or a generic functionfunc()T
, does not inspect that argument, but simply returns it later.To overcome this limitation, API developers must artificially bake in errors in their APIs, for example:
or:
both of which are suboptimal because even though this API is error-agnostic, this property was lost and it's not longer reflected in its type or enforced by the typesystem.
With
ErrorOr
, the burden of deciding on whether the API needs error handling or not goes away, and instead API callers have a free choice on whether to callDo
with an error-returning function (e.g.,func()ErrorOr[T]
) or not (e.g.,func()T
).Also, with
ErrorOr
the fact thatsingleflight.Do
is error-agnostic remains captured in the type and enforced by the typesystem. This is a property that is worth retaining.Another (small) benefit of the
ErrorOr
in this case is that thesingleflight.Group
implementation would be simpler because it would need to store only 1 value per call (i.e., theErrorOr[T]
) instead of storing 2 values per call (i.e., theT
and theerror
).Errors / values in channels, maps, slices
When I use channels to implement pipelines or glue computations together that can either return a value or fail with an error, I found myself having to define a new
struct
type to encapsulate the value / error to use in the channel, e.g.,chan IntOrError
(pre-generics).The
ErrorOr
would also be useful for channels, e.g,chan ErrorOr[int]
so we don't need to redefine new struct types for this use case likeValueOrError
(or a generic equivalent of that) because we can reuseErrorOr
instead.We could also store
ErrorOr[T]
in collections such as maps, slices, etc. For example, spawn 10 goroutines and have each store their result in[]ErrorOr[int]
. Another example, implement a cache with positive and negative caching, e.g.,map[K]ErrorOr[V]
.Naming problem with
err
When we use the pattern
(T, error)
, we need to define theerr
many times:In some cases, there are no new names on the left side of
:=
so we need a few tricks there, either by writingvar err error
:or by defining different names for the error:
With
ErrorOr
we don't need tricks:Inconsistent use of
(T, error)
There are APIs that are exception to 4 states of
(T, error)
rule, such as, theio.Reader.Read
. But they are the minority.The documentation for this interface method requires 4 paragraphs just to explain that this API can actually return a value and an error at the same time. If it were common practice in Go to return a value and an error at the same time, it would not be necessary 4 paragraphs of documentation to explain this notable exception.
Furthermore, the fact that it requires such as careful explanation is evidence in itself that this is a pitfall for API callers. And reading through the details it sounds very error prone and confusing.
A new type like
ErrorOr
could also be useful for a new API likeRead(...)ErrorOr[int]
because it would mean more consistency regarding error handling across APIs, we could also remove those 4 paragraphs of documentation, less pitfalls for developers, and also better type safety. API callers would need to either handle the value or the error, and there would be no ambiguity in that.Design ideas
One possible definition of
ErrorOr
is the following:The
New
andError
constructors only allow for either a value or error, they don't allow for both an error and a proper value.Related work
The
ErrorOr
has different names in other languages but in essence it's the same idea:Known issues
new(ErrorOr[T])
can create a state that is neither error not a proper value. Perhaps we would need special tooling or compiler support to prevent or produce a warning if a developer wrote this code, since it would be desirable to always use eithererroror.New
orerroror.Error
.Future ideas
This is out of scope for this proposal but I think it's an interesting, possible future extension of the
ErrorOr
idea, that can help with Go exceptions without actually requiring exceptions.Let's say we have a new operator (I will use
<-
but other syntactic choices are possible), then we could do the following:In this example,
OtherFunc
andOtherFunc2
are equivalent but inOtherFunc
we use this new operator to avoid having to explicitly write the error handling code. The new operator<-
does the automatic error handling for us by checking if theErrorOr
contains an error and if so, return that error to the caller. This may be an alternative to introducing exceptions without actually requiring full support for exceptions.Alternatives considered
An alternative to
ErrorOr
would be to treat(T, error)
like a tuple type so that a type likefunc()(T, error)
could be used to instantiate generic functions likefunc()T
. But this would be a much bigger change thanErrorOr
with far reaching implications for the language. TheErrorOr
is just a new type.More resources
Initial discussion was #48287 (comment)
The text was updated successfully, but these errors were encountered: