-
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: "collect": dealing the error handling boilerplate from another angle #32880
Comments
See also #25626 |
Though sharing name with #25626, I would argue they are very different. This proposal does not include anything like |
This is a good use case for hygienic macros: #32620 |
This is an interesting proposal, but I note that it doesn't support what is by far the most common case when an error occurs: returning from the function. It's true that if your function is all top-level calls to |
@ianlancetaylor Yes, it does not support returing from the function, and I deliberately propose so, since the biggest issue all error handling proposal I've seen so far has a common problem of obscuring control flow and exit points. I am aware of collect, being a builtin and a non-returner, would not cope well with things other than basic expressions; it is not designed to do so. I justify this by saying: However, like opionions I presented on the proposal, all these are based on experience of my own and those who are close to me, which by no means can match up the Go society; but I do not have ability to get more datas and opionions on them, so I am unclear about whether they are widely accepted or not. |
Since Meaning if
|
No, it skips if err is not nil. But if you stop using collect, but with the error-prone valuable, yes, it continue with zero value.
Yes, in current form, |
@leafbebop thanks for that clarification. What worries me is that after the first |
@mvndaai I understand that. But as I say in the proposal, Unlike other proposals ( |
In order to use this, essentially everything after the first call to Although Even in the base case, in practice every line will start will If you accidentally forget a second or subsequent -- for @golang/proposal-review |
Introduction
While there sure is boilerplate about current error handling in Go, there are many things that many people like about it - Go forces programmer to think about and handle errors manually; Go treat errors as value and there is no magic about them (so exit point and flow control are very visible). That makes me think that
if err != nil
is not boilerplate, instead, they are needed in a lot of cases. So it occurs to me that it is notif err != nil
being too much becomes boilerplate, but rather, them being too frequent is the problem.So, what if we handle error the same way before, but just less frequently? It sounds weird, but we already has that in some idioms. In the Official Go Blog post errors are values, Rob Pike promotes an idiom where errors are collected - but not handled until all other "steps" statements. The steps are simply skipped if there is an error collected before. It seems to me a good solution to many boilerplate caused by too frequently appearing
if err != nil
. However, it has limited uses since "steps" may not have the same signature and writing a helper wrapper type might as well be boilerplate.But with help of built-in magic, I think the idea of the idiom can be applied and thus improving much more cases. And I propose so.
Proposal
I propose a new built-in function called
collect
. Thecollect
built-in takes three arguments: a pointer to an error, a function call, and an error wrapper. If the error held by the pointer is nil, it returns what the function call returns except the last error, which is wrapped by error wrapper and then collected to the error pointer; otherwise, it returns zero values matches the signature of function call.collect
cannot be nested - which means thatcollect
appearing in anothercollect
will trigger a compiler error.An error wrapper is a function with signature
func(error) error
- it takes in an error and returns the wrapped error. If the wrapper isnil
, the error is collected without wrapping. If in rare case that the wrapper decides to return nil, nil is collected.In psedu-code,
collect
can be written like:To avoid bugs from recycling error pointers, consecutive uses of
collect
should be considered as declaration to the errors, so it can triggerdeclared and not used
error from the compiler.To reduce boilerplate of writing wrapper helper function, standard library (preferably
errors
package) should provide factories to wrappers, such as:Example
The example from draft design overview:
The example of printing sum:
As for the example using nested
try
,collect
shall not be nested.The main function from rsc's unhex program can be simplified without splitting:
Since
collect
does not handle errors, language tools to deal with errors can still work well:Pros and Cons
collect
does not interfere control flows (of the callers). Thus exit point remains very visible.collect
does not handle errors. Code still needs to do that to avoid compile errors. So the language still encourages (or forces) people to think about errors and handle them gracefully.collect
collects error so error handling may happen in a concentrated or unified way, which will make the happy path cleaner and more visible, and what happens to errors remain clear.collect
does only minimal work, so errors remain values and language tools to program around values can still work well.if err != nil
Discussion
name
Naming things is hard and I don't seem to be good at it. While
collect
reflects the fact that it collects error, it does not reflect the more important part: skipping function calls when error has already occured. However, how to summarize that and form a better name is beyond me.defer
andgo
It is quite obvious that
defer collect(...)
makes no senses at all. Therefore, I think it is best to ban this kind of usage. On the other hand,go collect(...)
might be valid, but it is prone to (if not always leads to) data races (on&err
). Hence, I would suggest to ban this kind of use too.One interesting case to note is needs for cases like
collect(&err, defer f(), w)
. While it is invalid go syntax, I can think of many cases that would need it. I haven't decided whether or not to allow such uses.If allowed, the previous example can be written as:
func CopyFile(src, dst string) (err error) {
wf := Wrapperf("copy %s %s: %v", src, dst)
var err error
}
for
While it is easy to use
collect
in a loop, there is two problems.The first problem is that
collect
can only help to skip a loop if you wrap the loop into a function literal. It sure looks ugly, but maybe it is better to check errors manually before entering a loop.The second problem is that careless use of
collect
might create a busy loop. Vet should handle this case.chaining / nesting
There are two problems about chaining / nesting.
First problem is that since
collect
enables chaining in places likeos.Open(file)
, it is prone to less visible need to writedefer
statement. But unliketry
, there is at least a trace that an error is collected, which might remind the programmer to deal with it more carefully.The second problems is that nesting/chaining obscures control flow in a hidden way - the reader (or even writer) of the code has to decide which error gets collected first since order of collecting can change behavior.
Therefore, to avoid confusing behavior, nested
collect
should be disabled. However, chaining, on the other hand, might be ok.performance
It is unsure to me whether using
collect
would hinder performance of function calls or the compiler can optimize it.The text was updated successfully, but these errors were encountered: