Proposal: New keyword returnif (resolves error handling)

390 views
Skip to first unread message

Jeremy French

unread,
Nov 9, 2020, 2:20:29 PM11/9/20
to golang-nuts
First, the caveat.  I know error handling is a long-standing discussion.  I know there has been lots of debate on error handling, and it can seem like there are no more new ideas to be had on the topic.  And I have looked at several/most of the most popular proposals, and there are quite a few that bear similarity but are critically different in important aspects to this proposal. If this has been proposed before with the same effective fingerprint, I haven't been able to find it after several hours of due diligence.  I'm interested in hearing any and all (well-reasoned) thoughts on the matter, but if there's a flaw in my logic, I don't see it yet.

In short, the proposal is to create a conditional return statement, e.g. "returnif" of the form:
   returnif [bool], [returnvalue],...

This is syntactic sugar for:
if [bool] {
    return [returnvalue],...
}

of which, the immediate benefit is:
returnif err!=nil, err

or alternatively:

returnif myErrorChecker(err), myErrorWrapper(err, "Some text.")

Here's my reasoning.  Go Error Handling in is current form is extremely correct, precise, explicit, and clear.  All very good things.  Really the only problem with it is the repetitious verbosity.  A programmer's instinctive reaction to repetitious verbosity is to wrap it in a function.  The infamous "if err != nil {return err}", although repetitious and ubiquitous, cannot effectively be encapsulated to "return someErrorChecker()", because the return statement is unconditional.  Once I start a statement with return, nothing I can do later in the statement or within a called function can change whether or how that return alters flow control.  This, I think, is the quintessential problem with the current error handling methodology.  This proposal addresses that without sacrificing any of the good things about error handling in Go.  Error handling is still explicit.  Errors can still be treated as values. Proper error handling and annotating is blessed but optional. The behavior of defer is unaffected.  It is still simple to understand, and easy to read. And it's entirely backwards compatible.  

Also, while the most obvious benefit is in error handling, this is not technically just an error handling solution. It is completely unopinionated on the type of values it returns, or whether they qualify as an error or not.  I can foresee enterprising gophers finding other uses for this keyword, and quite possibly even new useful design patterns could emerge as a result.

Possible Objections:
  • It could be seen to violate the "one way to do things" principle.  However, 
    1. It violates this rule much less than almost all of the other proposals for error handling.
    2. If, under the covers, it's just code substitution, then there's still only one actual avenue of execution in the compiled objects.
    3. There is precedent for this type of shortcut when the benefits are so widespread and the sugar improves readability. For example,
      } else if isTrue {
          doSomething()
      }

      is sugar for
      } else {
          if isTrue {
              doSomething()
          }
      }
  • "It's just a variation on other existing proposals."
    • This proposal avoids or addresses all the objections listed in the error handling meta issue #40432, and as such, may be a variation, but varies sufficiently to create a different result set.
    • From the meta issue:
      • The check/handle proposal.
        • One major reason this was rejected was a lack of clarity between handle and defer.
      • The try proposal.
        • One major reason this was rejected was the additional flow control: a complex expression using try could cause the function to return. Go currently has no flow control constructs at the expression level, other than panic which does more than just return from a function.
      • Special characters, often ! or ?, that insert an error check in a function call or assignment.
        • These are typically rejected because they are cryptic. Often a single ! or other character leads to a change in flow control.
      • Simplifications of if err != nil, to reduce boilerplate.
        • These are typically rejected either because they don't reduce the boilerplate enough to make it worth changing the language, or because they are cryptic.
  • What about edge cases?  How to handle else clauses or additional conditional logic based on error type etc.?
    • It's my belief that else clauses too rare to justify additional syntax.  If you need an else/else if clause, you can use the existing syntax and lay out your conditionals on more lines. Also - you know - any code after a return statement is essential an else clause anyway.
    • By making [bool] an expression, any additional logic may be handled by the programmer in a determinant function that returns a boolean.  This puts this type of flow control in the hands of the developer.
    • The short statement currently available with if and for statements (if err:=doSomething(); err != nil) could be implemented in a similar fashion, but my personal vote would be to disallow it, as most of the simplicity and clarity of this proposal could be lost down that rabbit hole.
I believe the most critical difference between this proposal and previous ones is that this proposal addresses the core issue more directly.  The central problem to the current error handling methodology is not actually specific to error handling.  That's just where it's most visible.  The core problem is essentially the fact that a child function cannot affect the conditional return of a parent function (barring further conditional logic), even with explicit permission by the parent function.  This is not true with any other form of flow control.  This is why the current methodology feels wrong to developers, because they are disallowed from encapsulating repetitious logic in a way that is consistent with other flow control statements. 

Anyway, that's my argument.  If anyone knows of a previous proposal that this duplicates, and/or knows why that one didn't/couldn't work, I'd be grateful for the explanation.

Ian Lance Taylor

unread,
Nov 9, 2020, 2:58:26 PM11/9/20
to Jeremy French, golang-nuts

Jeremy French

unread,
Nov 9, 2020, 4:53:20 PM11/9/20
to Ian Lance Taylor, golang-nuts
Hm, yep. Very similar. Interestingly, the only cohesive objections I can see in those threads are to the minor details that are different from what I'm saying. Still, though - at least the idea has been presented before.

Jeremy French
607-444-1725

Tyler Compton

unread,
Nov 10, 2020, 6:00:04 PM11/10/20
to Jeremy French, Ian Lance Taylor, golang-nuts
Some thoughts:

The "try proposal" and some others identified and solved two problems:

a. The verbosity of using a full if statement to check error values
b. The verbosity of applying wrapping and other error handling logic to every place where an error is returned

This proposal only addresses Problem A. Maybe that's okay, because it seems like addressing Problem B often leads to complicated control flow issues.

I do like that this proposal doesn't treat errors in a special way. I could imagine it could be useful when checking if a key is in a map, for example.

value, ok := myMap["value"]
returnif !ok, errors.New("Oh no! Missing key 'value'!")

Another thought: Should returnif allow a statement before the boolean expression like if does?

returnif value, ok := myMap["value"]; !ok, errors.New("Oh no!")

That could make code more compact but looks very busy to me. My first impression is that this shouldn't be allowed.

I think I would be okay with reading and writing code like this. The level of complexity here is certainly lower than other proposals, although the value is arguably lower as well since it doesn't solve as many problems. I'll defer to others here.


--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CA%2Bj6mhDjFAjpQTcpA3oJfwFKUYdhNn-1jB39FiYoe6nspUTg2g%40mail.gmail.com.

Axel Wagner

unread,
Nov 10, 2020, 6:12:18 PM11/10/20
to Tyler Compton, Jeremy French, Ian Lance Taylor, golang-nuts
IMO, `returnif <bool>, <expr...>` reads awkwardly. If anything, it should really be `return <expr...> if <bool>`. At which point it also becomes clear that this isn't actually much of an improvement over `if <bool> { return <expr...> }`.

On Tue, Nov 10, 2020 at 11:59 PM Tyler Compton <xav...@gmail.com> wrote:
The "try proposal" and some others identified and solved two problems:

a. The verbosity of using a full if statement to check error values
b. The verbosity of applying wrapping and other error handling logic to every place where an error is returned

This proposal only addresses Problem A. Maybe that's okay, because it seems like addressing Problem B often leads to complicated control flow issues.

The way I think about it at least, is that the try proposal was rejected *because* it solved problem A specifically (and the returnif mentioned here doesn't; `returnif` stays a statement).

The main criticism of try, as far as I remember, was that in something like
`F(try G(), try H())` it wasn't clear enough how the control flow worked. But that's a direct consequence of creating an error handling *expression* (as opposed to a statement).
 
Realizing that was the point where I kind of gave up on the idea of ever "solving" the error handling problem. To me, error handling requiring a statement is the main gripe of most people - it's why it seems verbose and repetitive to people. But if that's so, but the community rejects any proposal that makes it an expression, then the problem becomes unsolvable.

Jeremy French

unread,
Nov 10, 2020, 8:42:20 PM11/10/20
to golang-nuts
On Tuesday, November 10, 2020 at 6:12:18 PM UTC-5 axel.wa...@googlemail.com wrote:
IMO, `returnif <bool>, <expr...>` reads awkwardly. If anything, it should really be `return <expr...> if <bool>`. At which point it also becomes clear that this isn't actually much of an improvement over `if <bool> { return <expr...> }`.

"return <expr...> if <bool>" is certainly a very viable alternative.  With the slight downside of possible confusion over order of evaluation.  Is the <bool> evaluated first as a short circuit?  Or is <expr...> evaluated first since it comes first on the line?  But that could probably be solved with good documentation on the matter.  Regardless, if "return <expr...> if <bool> were implemented, I'd still dance a jig.  The problem with "if <bool> { return <expr...>} is that the solution expands beyond the problem.  Once you allow a one-line if statement, you have no leg to stand on for enforcig the code formatter's bracketing rules.  Whether or not that ok with you, it's a philosophical issue for Go, and I think you'd have to win that war first.  I agree with you that "if <bool> { return <expr...>} is the simplest, most natural progression from where we are, but you'd lose something of Go's character in the process, and I think that will be a harder sell.
 

On Tue, Nov 10, 2020 at 11:59 PM Tyler Compton <xav...@gmail.com> wrote:
The "try proposal" and some others identified and solved two problems:

a. The verbosity of using a full if statement to check error values
b. The verbosity of applying wrapping and other error handling logic to every place where an error is returned

This proposal only addresses Problem A. Maybe that's okay, because it seems like addressing Problem B often leads to complicated control flow issues.

The way I think about it at least, is that the try proposal was rejected *because* it solved problem A specifically (and the returnif mentioned here doesn't; `returnif` stays a statement).

I certainly can't speak for the whole community, but for me, the key word here is verbosity.  I don't care (too much) whether it's technically a statement or an expression or a macro. What I care about is signal to noise ratio in my code.  What I really want in my fantasies is for my code to be 100% happy path. That's when it's most readable as a story.  Error handling is a very necessary evil, but I'd like it to not distract from what I'm trying to say.  In the most contrived example of a function that does nothing but call other functions (with errors), current error handling has a signal to noise ratio of 25% in terms of lines of code.  I'm wondering if people object more to that or to the fact that the surrounding "if" qualifies as a statement rather than an expression.  (I don't pretend to have been part of all these conversations.  Perhaps that is some people's objection.)

Also, to Tyler's point, I think returnif does apply to Problem b to a lesser extent, in that it allows you to abstract and relocate your error wrapping and error handling logic to a separate function.  It seems like, even in the most convoluted scenarios, you could probably boil all of your error handling down to a handful of error handling functions with different parameters, and then choose which one is appropriate in each use case.  Again, the point is to take all the "noise" and relocate it somewhere where I can go find it if I want (it's not implicit or magic), but it doesn't distract as much from the main story of the parent function.  I'm speaking here of the usage:  returnif doWeNeedToStop(err, variableThatHelpsDecide), formatOrWrapOrLog(err).  All the power is in the hands of the developer to choose what parameters are passed to either the <bool> or <expr...> but they can both be encapsulated in separate functions, thereby removing a significant amount of noise from the main storyline.  

Incidentally, this is also why I think this version of returnif is critically different from the version that reads "returnif <err>" because it doesn't require the compiler/runtime to know what an error is or when it should qualify as something that needs to be returned. In the two-expression version, the developer is in complete control.

--

Axel Wagner

unread,
Nov 11, 2020, 2:09:19 AM11/11/20
to Jeremy French, golang-nuts
On Wed, Nov 11, 2020 at 2:43 AM Jeremy French <ibi...@gmail.com> wrote:
I certainly can't speak for the whole community, but for me, the key word here is verbosity.  I don't care (too much) whether it's technically a statement or an expression or a macro. What I care about is signal to noise ratio in my code.  What I really want in my fantasies is for my code to be 100% happy path.

The majority (or maybe the loud minority - either way, definitely Go's design philosophy) of the community disagrees with you here.
Error handling should be explicit, that seems to be a hard requirement.
 
I'm wondering if people object more to that or to the fact that the surrounding "if" qualifies as a statement rather than an expression.

What I'm saying is that one causes the other. You mention the 25% ratio. That ratio is based on line-count and the only reason why the error-handling takes up more lines, is because it uses a statement. So my point is, to meaningfully reduce the "verbosity", you have to get away from that.

Incidentally, this is also why I think this version of returnif is critically different from the version that reads "returnif <err>" because it doesn't require the compiler/runtime to know what an error is or when it should qualify as something that needs to be returned.

I agree. I disagree that it's critically different from what we have today.
 

Jeremy French

unread,
Nov 11, 2020, 11:01:37 AM11/11/20
to golang-nuts
On Wednesday, November 11, 2020 at 2:09:19 AM UTC-5 axel.wa...@googlemail.com wrote:
On Wed, Nov 11, 2020 at 2:43 AM Jeremy French <ibi...@gmail.com> wrote:
I certainly can't speak for the whole community, but for me, the key word here is verbosity.  I don't care (too much) whether it's technically a statement or an expression or a macro. What I care about is signal to noise ratio in my code.  What I really want in my fantasies is for my code to be 100% happy path.

The majority (or maybe the loud minority - either way, definitely Go's design philosophy) of the community disagrees with you here.
Error handling should be explicit, that seems to be a hard requirement.

I think you're misunderstanding what I'm saying.  returnif (or return if) is clearly explicit and I've extolled that as one of its strengths.  I'm merely saying that in my fantasy world, there are no errors to handle, and everything works correctly the first time, and I can code as fast as I can think.  I know that's not realistic, and errors are a part of life.  My point is that they're not the main purpose of the code, and thus shouldn't be taking up (up to) 75% of the code space when I'm trying to read what a function does. (btw, I recognize LOC isn't the only measure of verbosity.  I think it's the most relevant, because A) it's directly related to how much of the code you can view on the screen at one time and how much you have to scroll, and B) the western world reads left to right, top to bottom.  Psychologically, that makes lines more heavy than words.)
 
 
[...]

What I'm saying is that one causes the other. You mention the 25% ratio. That ratio is based on line-count and the only reason why the error-handling takes up more lines, is because it uses a statement. So my point is, to meaningfully reduce the "verbosity", you have to get away from that.

[...]

I agree. I disagree that it's critically different from what we have today.

*shrug* As you said, returnif is still a statement, and yet it reduces the minimum boilerplate from 3 lines down to 1.  That seems pretty different to me.   I agree that it's not _conceptually_ very different from what we have today, and that's one of its strengths, too.  A small change for a big effect.

It sort of seems like maybe one of your goals is to find a solution that everyone will agree with.  And if so, then I agree with you that that becomes an impossible task once a community reaches a certain size.  I'm really just trying to find an objectively better solution to what we have now, and I think the reason returnif works better than the other proposals, is only because it recognizes the problem more correctly.  It's not really an error handling issue.  It's a flow control one.

--

Axel Wagner

unread,
Nov 11, 2020, 11:34:14 AM11/11/20
to Jeremy French, golang-nuts
On Wed, Nov 11, 2020 at 5:02 PM Jeremy French <ibi...@gmail.com> wrote:
*shrug* As you said, returnif is still a statement, and yet it reduces the minimum boilerplate from 3 lines down to 1. 

There's a way to get that without a language change - you can have gofmt make `if err != nil { return … }` into a 1-liner. It's virtually identical, apart from some extra braces and a re-shuffling of some letters. And it has been proposed in the past. It didn't get anything close to broad support.

It sort of seems like maybe one of your goals is to find a solution that everyone will agree with.

Far from it. In my opinion, `try` was by a wide margin the best solution to the problem proposed so far. So if we'd do anything, I'd hope it would be that. Even though many, many people disagree with it (obviously - that's why it was rejected).

But I'm not trying to be normative here. I'm trying to be predictive. I've been reading and participating in this discussion for years and I've seen dozens and dozens of these designs and read the argument on both sides. I'm not trying to say your idea *should* be rejected, I'm saying that I feel certain that it *will* be rejected. Not because "not everyone will agree with it", but because not enough and/or not the right people will.

Wojciech S. Czarnecki

unread,
Nov 11, 2020, 3:06:09 PM11/11/20
to golan...@googlegroups.com
Dnia 2020-11-11, o godz. 08:01:37
Jeremy French <ibi...@gmail.com> napisał(a):

> I know that's not realistic, and errors are a part of life.
> My point is that they're not the main purpose of the code, and thus shouldn't
> be taking up (up to) 75% of the code space when I'm trying to read what a
> function does.

There are fold spells to assist you in almost every editor otherwise suitable for coding.

The psychological miracle of the verbose 'if err' block is that in many it spurs a wise laziness: "ok I have typed ife<tab>, I have my cursor inside the block, I am in scope, so why not to put my failure-dealing code here, and now".


As for personal opinion:
the very "return-if" sounds bad to me. It blatantly and loudly encourages "let others care 'bout that" attitude. In real code (that accidentally is not a kitchen-sink math library) dealing with *failures* is the bread and butter. In server's code piece it is not unusual to have some 10 lines of happy path and next 1000 just to deal with failures that *we think* may happen. Next 1000 will appear in the maintenance phase when heavy load will reveal failure scenarios we never thought may exist.

The Go team's "try" proposal (I assume you read) was called off for exact same reason: "try" construct has been encouraging what many Go programmers assumed as a bad practice.


Hope this helps,

--
Wojciech S. Czarnecki
<< ^oo^ >> OHIR-RIPE

Henry

unread,
Nov 11, 2020, 10:35:18 PM11/11/20
to golang-nuts
Verbose error handling has its own merits. It is easier to scan, particularly when you are looking whether you have handled certain cases. It may be repetitive to write, but it is easy to read and it stands out.

In a language that offers multiple ways to handle an error, you have to slow down and read more deliberately. It is taxing for the code maintainers.
Reply all
Reply to author
Forward
0 new messages