I said earlier that I didn't want to get bogged down on specific examples, and this is what I meant - I know that there absolutely are better ways to implement the specific example I gave, I was just trying to demonstrate that Go not only already allows "expressions that change control flow", but in fact has explicit special cases in the language in order to make it possible. The "isGreen" example was just to demonstrate this - I know that this particular example could be reimplemented to sidestep the problem, but I've seen code where this wasn't true (I didn't include any here because the examples I can think of are difficult to understand without reading the whole of a library first, and I obviously don't expect anyone to do that just so that I can make an example).
Reflecting on this more, I think there are several distinct points that I'm arguing here. I'll try to make them explicit as I think them through:
1. There are places where streamlining error handling is acceptable and encouraged.
I don't mean that it's *always* good, or even "usually acceptable", just that cases do exist where it's desirable.
The evidence for this is strong:
* As I mentioned earlier, Go has special case syntax for certain operations like type assertions explicitly to streamline error handling. If streamlining errors was never a good idea, then Go would require you to write f, ok := x.(Foo) even if you know for a fact that x cannot be anything but a Foo, the way it currently requires you to write b, err := Bar(x) even if you know for a fact that x is a valid argument for Bar that won't cause an error.
* The
Ondatra library I linked earlier, as well as other test helper libraries I've seen, use t.Fatal in the same way - they judged that "if got, want := ondatra.DUT(t, "name").Config().System().Hostname().Get(t), "localhost"; got != want { t.Errorf(...) }", which Fatals if there is no "name" entry in the DUT table, if the attempt to fetch the hostname fails, or if the hostname is unset, was still preferable to making people write out the fatal checks themselves every single time, even though it means that if you don't actually want this to terminate your test you have to
workaround it with panics and recovers.
2. Streamlining doesn't have to mean using unchecked exceptions like panic() or t.Fatal().
All of the dozens of error handling proposals I've read so far are fundamentally aimed at letting you use the return value of a function that might return an error in contexts other than just assignment, and between them there are lots of different examples of errors that are more streamlined but still must be handled. My proposal is an example of one way to do this, but it is not the only way.
3. Streamlining shouldn't only apply to error handling that terminates the function.
Unlike panics, errors are values, and should be treated as such, which means that the calling function should be able to decide what to do, and this should include continuing. Frequently it won't - a lot of error handling is just return fmt.Errorf("more context %w", err) - but any proposal that assumes that it *always* won't is, IMO, confusing errors with panics. This is the question that first started this thread - I didn't understand why all the existing error proposals explicitly required that a function terminate when it encounters an error, and AFAICT the answer is "because people are used to thinking of errors more like panics than like return values".
4. The best way to have people write high-quality code is to make it easier to write high-quality code.
Nobody should ever have to look at their code and think "My API would be easier to use in normal circumstances if I just panicked in abnormal ones..." and then have to decide whether it's more important that their callers have a streamlined API or a principled one.
This is not hypothetical - I talked to some of the engineers behind the Ondatra project that I linked above, and they went through exactly this, where they wanted to return errors but their users all felt it made the tests too verbose, and that's why they use t.Fatal() everywhere.
I didn't start this thread with the intent of creating a new error handling proposal - I just wanted to understand why all the existing proposals don't care about point 3. But the more I think about this, the more I'm surprised it hasn't already been proposed.
Is part of the problem that the discussion around the try/check/handle/etc. proposals got so involved that nobody wants to even consider anything that looks too similar to those? Would it be more palatable if I proposed it with names that made it clearer that this is about the consolidation of error handling rather than an attempt to replace it entirely?
onErrors {
if must Foo(must Bar(), must Baz()) > 1 {
...
}
} on err {
...
}
Thanks,
Dan