First of all, the try approach in #32437 is a huge improvement to what we have now. So, I'll be happy if that moved forward. The following proposal is slightly more radical.
Summary: Introduce tryfunc that removes all error handling boilerplate within a function.
Original:
func f1(in int) (out int, err error) {
val, err := f2()
if err != nil {
return 0, err
}
if err := f3(); err != nil {
return 0, err
}
if err := f4(); err != nil {
return 0, fmt.Errorf("f4 failed: %v", err)
}
return f5()
}
New:
tryfunc f1(in int) int {
val := f2()
f3()
if err := f4(); err != nil {
return 0, fmt.Errorf("f4 failed: %v", err)
}
return f5()
}
tryfunc implicitly adds an (unnamed?) error return parameter to the function.f() that returns an error within a tryfunc will be equivalent to try(f()).try._, ok construct, one can explicitly request the error from a function, and that will automatically disable the error handling for that call.tryfunc can be a func or a tryfunc.I see lots of :-1: reactions and no comments so I'll at least offer my reasoning for the :-1:. There's a lot of magic that happens in a tryfunc, and it takes away a lot of the explicit clarity go code usually provides. It's difficult to know at a glance which function calls within the function can cause errors and which can't. Plus, if the signature of one of those functions changes to add an error return, your tryfunc behavior will change silently. In your small example these problems aren't such a big deal, but more complex real-world code I would expect both these things to cause real bugs and real confusion.
I'll add a comment here about why there may be so many :-1: reactions, I recently added mine and I usually don't like to :-1: without a comment. Explicit error handling is important, if an error ever happens, you want to have _some_ way of knowing which lines could have possibly caused it. For instance, take a look at this Java code:
public class Main {
public static void main() {
try {
doStuff()
} catch (Exception e) {
System.out.println("An error occurred!")
}
}
public static void doStuff() {
callA()
callB()
// ....
callZ()
}
// Implementations of callA, callB, etc...
}
If an error occurs, there is no way to figure out which calls could have even possibly thrown an error. Some of callA, callB, etc may throw errors, but not all of them.
The same thing in Go, with try:
import "fmt"
func main() {
err := doStuff()
if err != nil {
fmt.Println("An error occurred!")
}
}
func doStuff() error {
callA()
try(callB())
callC()
callD()
// ...
try(callZ())
}
// Implementations of callA, callB, etc...
This time, we can tell which functions might have thrown the error. It doesn't look quite as useful in my example, but in real-life applications it makes a huge difference to be able to know which functions may return an error.
What this proposal does is allow changing the doStuff function to look like:
tryfunc doStuff() {
callA()
callB()
callC()
callD()
// ...
callZ()
}
This doesn't annotate which functions are returning errors, nor does it even show itself returning an error (when in fact it does). Not to mention that a tryfunc is essentially just a func, and Go doesn't like non-orthogonal features :wink:
I was just trying to think outside the func :). In any case, this is a radical proposal, and for all you know, it may not be worth it. But here's the premise:
The influence comes from the type of systems I mostly work with: distributed systems where you are called from a remote system, and have to eventually call out into an external service, which can fail. In these situations:
If all functions in a program were tryfuncs, then it's not magic; It's understood that any function can return an error, and if so, it's guaranteed to be sent up the stack.
However, I can think of one really bad case: if someone changes a tryfunc to a func, then all errors will fall on the floor.
This proposal does not have much support. Some cogent objections were expressed above, and we agree with them. Therefore, this is a likely decline. Leaving open for one month for further discussion.
There were no further comments.
Most helpful comment
I see lots of :-1: reactions and no comments so I'll at least offer my reasoning for the :-1:. There's a lot of magic that happens in a
tryfunc, and it takes away a lot of the explicit clarity go code usually provides. It's difficult to know at a glance which function calls within the function can cause errors and which can't. Plus, if the signature of one of those functions changes to add an error return, yourtryfuncbehavior will change silently. In your small example these problems aren't such a big deal, but more complex real-world code I would expect both these things to cause real bugs and real confusion.