go: proposal: Go 2: `on err` or `catch` statement for error handling

The try() proposal #32437 has met with significant complaint, especially about a) lack of support for decorating/wrapping returned errors b) lack of support for widespread alternatives to returning error, e.g. log.Fatal(), retry, etc. c) obscured function exit d) difficulty of inserting debugging logic

If a separate line is usually required for decoration/wrap, let’s just “allocate” that line.

f, err := os.Open(path)  // normal Go \o/
on err, return fmt.Errorf("Cannot open %s, due to %v", path, err)

(This may overlap too much with if ... { <stmt> }. An error-specific variant is given in Critiques no. 1.)

This plugs easily into existing code, as it leaves the err variable intact for any subsequent use.

This supports any single-statement action, and can be extended with named handlers:

err := f() // followed by one of

on err, return err            // any type can be tested for non-zero
on err, return fmt.Errorf(...)

on err, fmt.Println(err)      // doesn't stop the function
on err, continue              // retry in a loop

on err, goto label            // labeled handler invocation
on err, hname                 // named handler invocation
on err, ignore                // logs error if handle ignore() defined

handle hname(clr caller) {    // type caller has results of runtime.Caller()
   if err == io.Bad { return err } // non-local return
   fmt.Println(clr, err)
}

Keyword on borrows from Javascript. It’s preferrable not to overload if. A comma isn’t essential, but semicolon doesn’t seem right. Maybe colon?

Go 1 boilerplate is 20 characters on 3 lines: if err != nil {\n\t\n}\n This boilerplate is 9 characters on 1 line: on err, \n

Specifics

// Proposed
on expression, statement

// Equivalent Go
var zero_value T // global, not on stack
if expression != zero_value {
   statement
}

// Disallowed; these require a named handler or function call
on expression, if/for/switch/func { ... }
on expression, { statement; statement }

The expression is constrained to ensure that its use (if any) within the statement is correct. For example, expression cannot be a function call or channel receive. It could be limited to an lvalue, literal, or constant,

Possible Extensions

For an assignment or short declaration with a single lvalue, on could test that value. (Note, I don’t recommend this; better to have on err consistently on its own line.)

on err := f(), <stmt>

An error inspection feature would be helpful…

on err, os.IsNotExist(err):  <stmt>
on err, err == io.EOF:       <stmt>
on err, err.(*os.PathError): <stmt>    // doesn't panic if not a match

on err, <condition>: <stmt>
on err: <stmt>              // this pair provides if/else in 2 lines

A condition list suggests parentheses:

on (err) <stmt>
on (err, <condtion>) <stmt>

Critiques

  1. The above may be too general-purpose. A post-assignment alternative is:

    err := f()
    catch <stmt>                  // test last var in preceding assignment for non-zero
    catch (err == io.EOF) <stmt>  // test last var and boolean condition
    catch (ep, ok := err.(*os.PathError); ok) <stmt>  // assignment preceding condition
    
  2. This doesn’t accommodate narrowly scoped error variables if err := f(); err != nil { ... }. A way to scope the variable to the stmt is:

    _ = f()
    catch err, <stmt>
    catch err, (err != io.EOF) <stmt>
    
  3. go fmt could just allow single-line if statements: Would it also allow single-line case, for, else, var () ? I’d like them all, please 😉 The Go team has turned aside many requests for single-line error checks.

  4. It’s as repetitive as the current idiom: on err, return err statements may be repetitive, but they’re explicit, terse, and clear.

@gopherbot add Go2, LanguageChange

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 59
  • Comments: 94 (38 by maintainers)

Most upvoted comments

I would opt for a ,= reverse nil operator that does the same but keeps the clean line of side

err := f() // followed by one of
err ,= return err            // any type can be tested for non-zero
err ,= return fmt.Errorf(...)
err ,= fmt.Println(err)      // doesn't stop the function
err ,= continue              // retry in a loop
err ,= hname err             // named handler invocation without parens
err ,= ignore err            // logs error if handle ignore() defined
err ,= { 
   do()
   things()
   return err
}
handle hname(err error, clr caller) { // type caller has results of runtime.Caller()
   if err == io.Bad { return err } // non-local return
   fmt.Println(clr, err)
}

@griesemer

I’m sorry to have overemphasized checkErr, I didn’t mean to suggest it was the default case in code that I review, I only wanted to use it as an example of how error handing in “real world” Go is probably a lot less sophisticated than one might think. I agree that try is a step up from checkErr, but then again, any use of returned errors instead of blanket panics would be an improvement.

I’m pretty sure more explanation from me won’t help to sell my case any more at this point, but I do want to respond to this:

I see your point that on err might be easier to change into proper error handling (i.e., decoration) but . . . rather than insist on decorating each error separately, use a deferred error handler (which can be one line) . . . seems like a much easier sell . . .

Prior to the check/handle proposal, I had never once considered, or seen in the wild, the general pattern of decorating errors with a deferred function. (The only times I’d ever used a deferred function to interact with errors were in the exceptional cases where that pattern was more convenient for complex resource cleanup.) And I’m not alone: I was just on a Go Time podcast panel where everyone else said roughly the same thing. While I understand your perspective that a one-line deferred error handler may seem like an easier sell—less typing, and less repetition—my experience tells me that I’d have a much harder time selling that sort of “look over here!” indirection as a general solution to novice programmers, versus short, in-situ repetition of on err expressions—of which we already have a reasonably effective (if more verbose) version in if err != nil.

I believe the try proposal significantly under-values the costs of physically separating error decoration from the error-generating statements, and of preventing many other kinds of error handling altogether. It seems to me that if you boil Go down to its barest essence, one of the properties that surely must remain is (waving my hands and trying to paraphrase a lot of stuff here) that errors are not exceptional, that the “sad path” of code is at least as important as the “happy path”, and that the control flow from error handling should not be treated any differently from the control flow of business logic. It seems pretty clear to me that try subverts this essential property, and that on err does not. And if, as @ianlancetaylor says,

I don’t think the savings are enough to justify the language change

Then let us not change the language.

I support this proposal.

It is important that any change to error handling allows each error in a function to be annotated explicitly and independently from other errors. Returning the naked error, or using the same annotation for every error return in a function, may be useful in some exceptional circumstances, but are not good general practices.

I also support using a keyword instead of a builtin. If we add a new way to exit a function, it is important that it isn’t nestable within other expressions. If we have to wait until further improvements to the language allow this to be accomplished without breaking the Go 1 compatibility promise, then we should do that, rather than make the change now.

@griesemer

Can you explain why you think on err is so much better than if err != nil that we need new syntax?

To be clear, I’m in the “leave if err != nil alone” camp, like many other veterans of the language. But it seems like the core team has decided this is a “problem” that’s worth solving, and I have sympathy for the perspective that it might encourage more not-yet-Gophers to become Gophers, so I’m speaking from that assumption.

edit: From that assumption, I think it’s important that any proposal not affect the semantics (for lack of a better word) of error handling as it exists today (by which I mean that errors are generally totally handled adjacent to the error-causing expression) and instead affect only the verbosity of error handling. And the only commonality between the error handling blocks I write which could be reduced are

if err != nil { // <-- this line
    ...         //
    return ...  // <-- this keyword
}               // <-- and this line

For that reason, I feel like on is about as good as it can get, I guess.

Also, there is a mantra that every error needs to be decorated somehow. I agree that this is important at the library boundary level. But there are plenty of package interior functions where decorating an error is not needed; in fact it only leads to over-decorated errors. Can you expand on the statement of bad practice that you are referring to?

I think this is one of many points that demonstrate a disconnect between the core Go team and Go as it is used in practice, at least as I have always experienced it. Over the years I’ve taught dozens Go courses, consulted for nearly a hundred companies, small and large, startups and Fortune-500; I suspect I’ve interacted with thousands of Go programmers, and seen nearly as many discrete Go codebases. They are almost exclusively private, and their developers infrequently respond to surveys. As I see it, this is the primary market for Go the language. (Of course, I may be wrong. Grain of salt and all.)

In these contexts there are almost no packages which are imported by more than one binary. In fact I’d estimate that ~50% of Go code I’ve seen has just been in package main. These developers rarely have enough time or experience to properly structure the main business logic of their programs, much less think deeply about the structure or cleanliness of their errors. Many, many developers have the instinct to write functions like this, and use them everywhere:

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

So, given this reality, where package boundaries are rarely well-defined, where functions instead serve as the primary way to hew borders in programs, the risk is never over-decoration. I’ve never once given a code review where I requested the author to de-annotate an error because it was too noisy. But I’ve given plenty of reviews where I’ve had to convince the author to remove their checkErr helper or equivalent and actually return errors at all rather than just terminating the process from somewhere deep in the callstack. So if a new language feature provides most of the “I don’t have to think about this” convenience of checkErr and requires something like a deferred block to do annotation, I’m positive that almost no developer as I’ve described them will perform any annotation whatsoever, and I fear we’ll move the resiliency and coherence of most Go codebases backwards, not forwards.

Of course, on err allows this to happen, too, but at least in code review we can suggest in-line amendments, which I believe would be more eagerly adopted.

@peterbourgon Thanks for your response.

It has become clear from much of this discussion that many are disappointed that try doesn’t do more for error handling. Maybe it’s not emphasized enough, but the proposal is clear that if decorating/handling an error at the point of the error check is desired, the existing if err != nil pattern is the recommended approach. We are at this point not convinced that there is anything else to do in that case (but see below).

But there is the legitimate use case where errors are simply returned. And there is the legitimate use case where all errors returned from an API entry point are decorated uniformly (the same way) - which is where the deferred handler is suggested (which is independent and orthogonal to try). We hear that these cases may not be the majority of cases, but they are not negligible, and they are the cases where the boilerplate becomes egregious because it adds only clutter. In those cases I would want to factor out that code, like one would with everything else that’s repeated and the same, but because we can’t do this in Go (and won’t be able to do this in any foreseeable future version of Go), the helper function try seems like a good compromise. It’s truly a minor addition to the language. If it doesn’t fit a specific program design, don’t use it.

I think there’s a fundamental misunderstanding in this general discussion (not between you and I), a fear that try is the “must-use” mechanism for error handling, or perhaps more accurately, that try might become the “must-use” mechanism. That is not our intention, nor does the proposal say as much.

Regarding the need for a more comprehensive error handling construct: philosophically, Go has never been in the business of promoting a specific programming paradigm (notwithstanding the fact that it is an imperative, statically typed, type-safe language). For instance, Go supports OO programming but doesn’t mandate it. Instead it provides all the building blocks (methods, interfaces, embedding) to achieve that. Go supports exception handling but doesn’t encourage programming with exceptions via a try-catch construct. Instead it provides the building blocks (panic, defer, etc.) to do so if desired. That is not to say that Go is not opinionated - of course it is. But that opinion is not enforced so much by the language but by convention and by tools (go fmt, go vet, go lint, etc.).

Whether the language or the tools enforce a paradigm doesn’t make much of a difference for a specific piece of software at a given time, the outcome is the same: a certain style is encouraged or enforced. But it matters in the long run: conventions can evolve as we learn more, and tools can change and adapt. Existing programs will continue to run. On the other hand, if we enshrine paradigms into the language it will become much harder or impossible to change them because it might invalidate a lot of existing code.

For that reason I am much more reluctant to make more significant language changes such as adding “on err” or the like (*). try is simply factoring code. on err is adding a new paradigm.

At this point, I think the question we need to answer is this: Is it reasonable to add a minor built-in to the language that facilitates error checking for a significant number of cases (but not most), and which can be safely ignored if one desires to do so, or is even that not justified and we should leave the language alone.

Thanks.

(*) Adding the predeclared type error was of course the first (and perhaps the biggest) step in this direction: It enshrined that errors should be of error type. But by making them more abstract (as error is an interface type) it increased interoperability between components while introducing a standard, yet errors remain “just values”. This was clearly a win-win.

As you hinted at yourself, the proposed syntax doesn’t gain much over just putting the equivalent if statement in one line:

if err != nil { return err }
on err, return err

I don’t think it’s worth making a breaking change to the language for something you can almost get with a gofmt tweak.

@campoy try is just a macro, fashioned into a built-in function. If we could write try in Go, one might write it and use it like any other function we use to factor out shared code (one sort of can, using ugly hacks with panic and defer, for specialized calls, but that’s beside the point). try doesn’t introduce new syntax or new keywords; and since it lives in the universe scope, it can be shadowed like any other name. As a consequence, it won’t affect any existing code.

on err is a new statement and requires a new keyword. It will interfere with any code that uses on as an identifier. It also overlaps directly with the if statement, something we try to avoid for major language constructs. The syntax on err, <statement> is unusual for Go, one would expect on err { ... }. I don’t know why there needs to be a comma. Details.

Of course we could say that on err is simply a form of syntactic sugar for a common pattern, very much like try. And that if one could write on err in Go some would (like some would with try).

But there is really a difference between a built-in function and a new statement. Built-ins can easily be ignored (because they don’t interfere with existing code). With few exceptions (len, append), they can be skipped when learning the language, and introduced on demand. In the spec they show up at the end. Several built-ins are not strictly “needed” and one could write the same code by hand (append, copy, new come to mind) - these are here purely for convenience. try falls into this category.

A statement cannot be ignored - it is either used or it must be avoided (in the sense that we cannot have identifiers that collide with keywords of statements). It must be learned by everybody for that reason. In the spec it shows up prominently.

Generally, a statement in Go doesn’t enshrine a single use case: An if statement is simply for conditional branching, a for statement for looping, a switch statement for selecting between many alternatives, and so on. They operate on arbitrary data types and in different kinds of coding styles.

But an on err statement is only for checking and handling errors. There’s no other use case. As such it will become the recommended way of checking errors, even though there is absolutely nothing wrong with if err != nil {}. So on err - by sheer existence - promotes a certain coding style, or what I called a paradigm (maybe paradigm is not the best word).

Not only does on err overlap with if, it also does not make code a lot more compact. Code that is now dominated by if err != nil will become dominated with on err - there’s not much gained, and a lot lost (introducing a new language statement comes at a big cost).

try also has a very narrow use case. It’s only good for one thing: checking an error and return. But it doesn’t affect existing code. Because it’s much more narrow than on err, the savings are much bigger. It’s also more powerful than on err because it can be used in an expression, something we can’t do with on err. Yet it comes at much less cost. It doesn’t scream “you must use me for error handling” in the language spec. It’s just a little built-in for convenience (see above). It terms of complexity of implementation it is trivial compared to say append, copy, etc. It is tremendously helpful in many situations, very much like append is, but it doesn’t have to be used.

One more thing, unrelated to your question: We (the Go team) have considered many many alternatives (some of which I mentioned in the design doc). But for minor syntax differences, most alternative proposals brought up in this public discussion we have already considered one way or the other (we have been discussing error handling internally on a regular basis for more than a year). We ended up with try because it seemed to have the least negative impact on existing code and the language, and because it seemed easy to explain and very clear. Where it can be used it has a big impact on the readability of the code (in our minds) because it reduces so much boilerplate. It opens new avenues of use because it can be used in expressions where before we needed separate statements. The same cannot be said for most alternative proposals that have been brought up so far (but the ideas that make error checking invisible and implicit - nonstarters). But it does require getting used to. It’s a bit of a shift in thinking about errors. It only solves error checking, not error handling. It takes time. And maybe the time is not now, and maybe try is not it. We don’t know without try-ing 😃

Apologies for the lengthy reply (I didn’t take enough time to make it shorter). I hope this helps explaining a bit better what I tried to say here.

I’m not sure what kind of help you are looking for from the Go team, or what further refinements are appropriate. You’ve gotten comments from at least three members of the Go team here.

As I suggested above, I personally am not in favor of this, as it adds a new keyword, and a new kind of statement syntax, and a new kind of expression, all to save the boilerplate of a comparison expression. Saving boilerplate is good but adding three new ideas to the language is a heavy cost.

To be very clear, I am not trying to say “therefore we should implement the try proposal.” I am saying that this proposal, viewed by itself, does not, in my personal opinion, provide a benefit that exceeds the cost. And I want to stress that the cost of a new kind of statement syntax and a new kind of expression is a high cost for a language like Go that aims to be orthogonal and to have a minimal number of concepts. And, again, to be very clear, it is perfectly fair to argue that the try proposal is also not orthogonal, but it does not follow that we should adopt this proposal.

Hey @griesemer, regarding the line

For that reason I am much more reluctant to make more significant language changes such as adding “on err” or the like (*). try is simply factoring code. on err is adding a new paradigm.

I’m curious about how is try simply factoring code when it’s able to make the function return? Not criticizing, just trying to figure out why try is seen as something “simple”, while on err is a new paradigm.

@ianlancetaylor

the savings are enough to justify the language change.

For me personally with try there are none savings only more mud. I am voting for the status quo, but if we’re thinking changes lets think about what a possible outcome of stepping back to C if expression semantics would be. Being implicit it might even tackle at immortal if interface == nil gotcha.

PS. (Edit)

I think the boilerplate that counts is what people see.

But wasn’t this given as a rationale for the try proposal? “On popular demand…”.

(I remember your rebuke to mine (at go-newbie times) proposal of iirc watch some years ago. **Spooky action at distance**. I was then wrong, you were right. Try will allow for the spooky action not only at distance but also well hidden in layers of wrapped calls.)

If the members of the Go team have personal objections to the try proposal, they will state them.

Are you discussing this because you see something in this proposal worth developing? You and the other team leaders aren’t commenting in other try() alternatives that I’m aware of. (I’d rather not invest in a moot debate 😃

I’m discussing this because you explicitly said, in https://github.com/golang/go/issues/32611#issuecomment-508959846

The proposal needs further refinement, but there’s no point without help from the Go team, and it looks like we’ll get that only if try() is abandoned after it’s been prototyped. They haven’t stated any willingness to include an alternative to try() in pre-release 1.14, sadly.

I felt that that statement was unfair, given that at least three members of the Go team had already commented on the proposal, and given that we try very hard to stay open to input at all times.

Also, you brought up the proposal on golang-nuts saying that it may have traction, although the downvotes appear to outnumber the upvotes at least at this time.

I see serious problems with this specific proposal, as I’ve expressed above. I’ve tried to point out problems I see in a number of other proposals as well.

I had the impression, and I believe it’s common, that you and the other team leaders generally speak for the team when you comment in the official forums.

That turns out not to be the case. I don’t think we could possibly keep up with the discussion if we had to get together and formulate an agreed-upon response before each reply. We’re not even all in the same timezone. And I’m sure there have been cases where we have publicly disagreed with each other.

@networkimprov

I added the catch variant described previously, and expanded on it to scope the error variable to the statement:

With all due respect, all that additions and refinements morph your initial simple solution into something that will need no less cognitive load as try. IMO.


@griesemer

(We have a few built-ins that we don’t strictly need: new can be done by taking the address of a variable, append and copy could be custom-written, complex I’ve already mentioned, and print and println are for convenience. try falls into a similar category in my mind; it adds convenience where it is applicable.

But yours try, while smart, makes writing convoluted paths convenient, using magic that (except autodereferencing) so far has not been used in Go spec.

And yes, if try becomes common practice, it probably will be introduced in a syllabus right after explaining how to use if err != nil

if is if, boolean expressions are 101, then an apprentice will need to learn and remember that:

[note that this is a description of reasoning, not a manual entry] try, while looks like a function call, is in fact not a try function call, but it is an invocation of a magic ~spell~ macro that will behave like a function call to the function given in try’s parenthesis, if and only if that real call will return nil in its last return parameter of type error position. Otherwise, as for the last return parameter of called func not being nil, try magically will morph into a plain naked return …never forget that the outermost, and outermost only, try incantation cuts off the last return parameter (usually err)! .

Try in its current shape adds too much convenience for too hefty price tag. Readability and simplicity-wise. Make it a statement at least, please, so it will have to occupy its own line. It is the try’s ability to nest that concerns. Frankly, I know I personally would be tempted to abuse it, then curse on myself later.

Now I believe no arguments remain for try() aside from . . . b) it’s a minor built-in that you can ignore.

I don’t really understand this point. From the perspective of a language user, there is no difference between a keyword and a builtin, except the way in which it is used. In either case, when they are learned is a function of their general applicability; it’s never been my experience that e.g. builtins are more likely to be learned later or ignored, as I believe has been implied.

More concretely, whether the error handling feature is a keyword or a builtin, it would certainly appear in the same place in a Go learning syllabus. Right? Could someone clarify?

@lootch Thanks for your comment. There’s definitively a social engineering aspect to all of this - we’re proposing to extend Go in unfamiliar ways, and if it’s the language you use day in and day out that is very concerning, perhaps even scary; after all it changes how one is supposed to “speak” in this language. You are absolutely right that there are a lot of emotions running through this discussion - I can’t see how a purely technical exchange would collect so much feedback in so little time. As to the decision, it’s still early days. I’m not the only decision maker here but I suspect we will stick to the process and let this run its course and decide how to proceed by the end of July.

Regarding the insurmountable issues with try: not to be facetious, but it seems to me that the existence of try is proof that these issues are not insurmountable 😃. Yes, try has odd requirements and behavior. The design doc is explicit on this subject. I note that new and make require a first argument which is a type, followed by regular arguments. No Go function has such a signature. unsafe.Offsetof requires a struct field (!) as argument - talking about an unusual signature. recover is only meaningful inside a deferred function and possibly stops an ongoing return/panic - another odd context-dependent requirement and strange control-flow behavior. Virtually every single existing built-in has some “code smell”, which is the very reason they are built-ins.

Regarding the claim that the try design flies in the face of Go’s philosophy or that it is an aesthetic failure: Since the proposal passed muster with the Go team, including several of the original Go designers before we published it, I’m going to make the bold statement that it is ok. As long as Go has a goto statement I think we can’t be feeling too smug about Go’s aesthetics.

But let me repeat what I’ve said elsewhere:

Please be assured that we have no intention to rush any new language features without taking the time to understand them well and make sure that they are solving real problems in real code. We will take the time needed to get this right, just as we have done in the past.

Is it reasonable to add a minor built-in … or is even that not justified and we should leave the language alone.

You already have a loud & clear answer to that question from the community.

Re “minor” built-in, please consider whether that’s a rationalization.

The bet here is that on err use at almost all call sites that yield an error delivers far greater benefit for essentially the same cost. Actually lower cost, since control-flow cannot become buried, and integration into existing code is trivial.

If data from a revised tryhard bears that out, what argument would remain for try()?

I was just thinking how inlining a handler would look like.

on err, { 
  do()
  things()
  return err
}

The comma doesn’t feel right in that case. This makes me think if using curly brackets instead of the comma is an option. However, the code then becomes more like the boilerplate that we have already with if.

I think option 3. from networkimprov has been underestimated (to quote): “go fmt could just allow single-line if statements: Would it also allow single-line case, for, else, var () ? I’d like them all, please 😉 The Go team has turned aside many requests for single-line error checks.”

If gofmt were to allow single-statement if-statements for err != nil to appear on a single line, visual clutter would be greatly reduced, without any change to the Go language.

I come from C and I greatly value the enormous effort and experience that has gone into making Go simple to understand and use. I also appreciate gofmt which removes a lot of trivial formatting decision making.

A 3-line boilerplate handball up the stack of an error from a called function is not the end of the world for me or anyone else.

But … in libraries I wrote in C, I returned an error value from almost every function and handballed it up the stack with a single line if statement.

Where an error needed to be originated, or a returned error needed to be decorated, I did that on multiple lines.

There was no error handling clutter, except where it counted: new error generation, and decoration.

Error generation, decoration and handballing was clear and uncluttered. And that was achieved by the C equivalent of:

if err != nil { return err }

@ianlancetaylor based on your comments here, I gather that you personally favor the request to “leave if err != nil alone”. Can you confirm?

I guess I’m not sure where that comes from. Personally, I think that we should try to do something about error handling, given how often it comes up as an issue for people learning and using Go. When people makes jokes about a language pattern (e.g., https://images.app.goo.gl/khYRQQUSYaGCEiwV9) they are pointing to a deficiency in the language.

catch <stmt> can apparently only appear after an assignment statement

Because it’s like an else. Other similar restrictions include:

Note that else can not appear as a statement by itself. You can’t even write

if x {
    fmt.Println("x")
}
else {
    fmt.Println("not x")
}

An else clause is intrinsically part of an if statement. So if catch is like else, it seems to me that there should be some direct syntactic connection between the assignment statement and the catch clause. I’m not sure I like that either, but I think it would be better than a standalone statement that is not really standalone.

The same applies to your default example; it is syntactically associated with a switch statement.

The restriction on writing func f() {} inside a function seems to me like a different kind of thing. It is not an example of something added, but rather of something prohibited. (I think the reason we don’t permit func f() {} inside a function is that we could never decide on the scope of the identifier f: is it visible inside the body of f? Is it visible to other nested functions that appear before f?)

the other forms of catch appear to me to be identical to if

No, they make implicit variable != zero_value tests, as with catch <stmt>.

Both examples are testing an implicitly defined variable err against a non-nil value. If there an implicit != zero test, I’m not sure where it applies. But, even accepting that, that is just saving a few characters over an if statement.

I am speaking only for myself here.

In this forum where you regularly speak for the Go team, I don’t believe anyone would absorb that claim.

I only occasionally speak for the Go team, but the fact that I occasionally do is why in a comment like this I am careful to explicitly state that I am speaking only for myself. I can’t control whether people absorb that, the best I can do is try to be clear and honest.

Here again, I am speaking only for myself.

@networkimprov For the record, I do not accept your distinction between “cost to users” or “cost to the ecosystem” and “cost to the spec.” I do not find that to be a useful line to draw.

As I’ve said multiple times in this issue, my comments above are specifically about this proposal. You keep bringing up the try proposal, but my comments above are not about the try proposal. I’m honestly not sure, but I think you are making an argument that is something like 1) we must do something about errors; 2) the try proposal is bad; 3) this proposal is better than the try proposal; 4) therefore, we should accept this proposal. If that is the argument you are making, I don’t agree with it.

I would say, instead, that I do not think this proposal is a good idea, and I do not think we should adopt it. I’ve explained my reasoning above.

We can then, separately, discuss the try proposal. It may very well be that the try proposal is bad and that we should not accept it. But even if we agree on that, even if we agree that we should not accept the try proposal, I do not think that we should therefore adopt this proposal.

If you reply to this comment, I encourage you to make an effort to not mention the try proposal. It simply isn’t relevant to the discussion of this proposal.

With regard to catch rather than on err:

  • the form catch <stmt> can apparently only appear after an assignment statement, a form of restriction that applies to no other statement in Go. It apparently implicitly refers to a variable set or defined in the previous statement, a form of implicit reference that occurs in no other statement in Go.
  • the other forms of catch appear to me to be identical to if, except that the curly braces are omitted. That seems fairly minor.

I am speaking only for myself here.

@ianlancetaylor, I’m afraid you’ve missed my point. The community is at the barricades about try() because we believe it would damage our code and/or the ecosystem of code we depend on, and thereby reduce our productivity and quality of product. That’s the “cost to users” I refer to. Perhaps I should call it the “cost to the ecosystem.”

This proposal does no such damage.

The “cost to the spec” as I define it includes the need to “explain to all users of Go” that a feature adds unique concepts to the language. As I’ve stated, the cost to the spec should be a far smaller concern than the cost to the ecosystem.

As this proposal has virtually no cost to the ecosystem, and roughly equal cost to the spec as try(), there’s really no contest.

Re “this is what an expression looks like, except … in an on err statement,” I addressed that with catch <stmt> in Critiques no. 1.

@peterbourgon I think it’s fair to say that a novice to Go will be able to program without try and do error handling - it’s not something that needs to be learned to get going (it’s what we do now, after all). But you’re right that once they start reading other people’s code, they will have to look it up. And yes, if try becomes common practice, it probably will be introduced in a syllabus right after explaining how to use if err != nil.

(We have a few built-ins that we don’t strictly need: new can be done by taking the address of a variable, append and copy could be custom-written, complex I’ve already mentioned, and print and println are for convenience. try falls into a similar category in my mind; it adds convenience where it is applicable. I haven’t heard the complaint that the presence of these “unnecessary” built-ins has made Go harder to learn due to the increased complexity.)

@ianlancetaylor there’s nothing like try() (either its arguments or effects) in the language now either, so apparently it’s ok to devise novel schemes to reduce error handling boilerplate 😃

What on err offers that try() lacks is application to virtually all errors, including recoverable ones.

Sure, the concept needs work to support a variety of use cases in a way that blends with the rest of Go.

I think the boilerplate that counts is what people see.

This reduces three lines to one line.

It reduces if err != nil {} to on err,, which by my count is reducing 12 characters to 6.

This adds a new kind of constrained expression which does not exist elsewhere in the language. The suggested extensions seem to mostly try to unconstrain the expression a bit by writing || as ,.

To me personally I don’t think the savings are enough to justify the language change.

Forgive me if this is the wrong place for this feedback.

Given that there is little enthusiasm for eliminating error handling boilerplate alone

I guess this may be getting at what Robert wrote in the closing comment to the try proposal, when he noted that it’s worth clarifying what the problem we’re trying to solve actually is. But I challenge this summary: in my opinion it is precisely and only the boilerplate that should be reduced, the current semantics—error handling as separate statements/blocks directly adjacent to the error-generating code—are important and foundational and should not be changed.

edit: Some questions that we should probably come to a rough consensus on as a community, before spending too much time on future proposals, include, in my opinion:

  • Are we addressing only specific, common error scenarios, or trying to improve all of them?
  • Are we only reducing boilerplate, or are we also changing error handling semantics?
  • Are we explicitly allowing chaining of multiple error-generating expressions in one statement?
  • (Related) Are we proposing a new keyword, a new builtin, or something else? Why is this best?

@networkimprov

This proposal does no such damage.

As this proposal has virtually no cost to the ecosystem, and roughly equal cost to the spec as try(), there’s really no contest.

You provide no evidence for any of these assertions.

The last one, in particular, makes no sense to me. try is a single built-in function which can be described straightforwardly in terms of if and return, and requires no changes to the language grammar. This proposal, on the other hand, introduces two new statements, one of which, on err, has a somewhat novel syntax for Go, as well as a new predeclared type caller. It gets even more complex if one incorporates some of the possible extensions you also propose.

@networkimprov Thanks for the comment. However, I don’t agree that it is possible to separate cost to the spec and cost to users. If the spec is more complicated, then it is harder for people to learn the language, and there is more for readers and writers to remember. Simplicity is a significant goal of Go, based on the assumption that keeping the spec simple means keeping the language simple means keeping it simple for programmers to learn and to use. If on err requires a different kind of expression, then we have to explain to all users of Go “this is what an expression looks like, except that you have to remember that in an on err statement there are various restrictions.” That is a cost to the spec and it is also a cost to users.

And, again, I am not talking about try. I am talking about on err. Each proposal must stand and fall on its own. It does not make sense to say “try is bad therefore we should adopt on err.” Perhaps try is bad, in which case we should not adopt it. But we shouldn’t take the arguments about try and use them to say that we should adopt on err. They are two separate and independent proposals. The only connection between them is that it is very unlikely that we would adopt both.

@griesemer

@campoy try is just a macro, fashioned into a built-in function.

Then why not onErr macro instead? One that only checks for err != nil, then if true executes return, goto or panic?

If we could write try in Go, one might write it and use it like any other function we use to factor out shared code (one sort of can, using ugly hacks with panic and defer, for specialized calls, but that’s beside the point). try doesn’t introduce new syntax or new keywords; and since it lives in the universe scope, it can be shadowed like any other name. As a consequence, it won’t affect any existing code.

It will badly affect all future code

[…]

But there is really a difference between a built-in function and a new statement. Built-ins can easily be ignored (because they don’t interfere with existing code).

No they can NOT be ignored while reading and comprehending the code. (And I personally am reading my own code ten times more often than I write it. Not to mention other’s code I am trying to dissect.)

With few exceptions (len, append), they can be skipped when learning the language, and introduced on demand.

IMO, only the complex, real and imag can be introduced on demand. All the rest must be understood, at least for their purpose and result, while onboarding into a project. How one could be supposed to read Go code without knowing of signature of the make, ehm?

In the spec they show up at the end. Several built-ins are not strictly “needed” and one could write the same code by hand (append, copy, complex come to mind) - these are here purely for convenience. try falls into this category.

No, try does not fall into that category. Neither append, nor copy, nor make have peculiar demands about other parts of the code nor they need augmenting prose in the specs.

A statement cannot be ignored - it is either used or it must be avoided (in the sense that we cannot have identifiers that collide with keywords of statements). It must be learned by everybody for that reason. In the spec it shows up prominently.

And thats good. We do expect all apprentices to learn that failures must be dealt with in clear.

[…]

because it can be used in an expression, something we can’t do with on err.

Having try as a function allows for, if not encourages to, hard to notice slips (pointed at #32437 discussion). I can add to this: how a debugger output will look like to set my attention at a fifth wrap of the tryies oneliner matrioshka? And how much effort will it demand from debugger authors?

Yet it comes at much less cost. It doesn’t scream “you must use me for error handling” in the language spec. It’s just a little built-in for convenience

An the false (write time only) convenience of it will make it hard to eradicate from the Go newcomers minds, so we will soon see a 20 cases switch at the topmost defer then down the body single lines dense of nesting try-parenthesis soup. LISPers will be happy, Gophers will not.

@campoy try is just a macro, fashioned into a built-in function.

onErr as a built-in macro will be yet more trivial to implement without endangering Go’s purity:

OnerrStmt = "onErr" ( "continue" | "break" | "return" | "goto" | "panic" )

Onerr macro executes a single terminating statement `return`, `goto`, or
`panic` if a variable named `err` is not `nil`. Otherwise its a noop.

(excuse me Lucio, its yours “onerr” words and my simplified version. We all are in quest for the most gain from the least changes set 😉

(edit: added continue and break allowed statements. I’d like to have also fallthrough but its likely too much demand compiler-changes wise)

I don’t get the point of “but if we have on line if-statements”. We do now have them, and AFAIK the go team doesn’t want them. We don’t have them for a reason. If we add them that would pretty much nullify this proposal, but that doesn’t mean that this proposal is any worse because of it. this has a clear advantage over on-line if-statements, it is very clear what is going to happen, and is less general then an if-statement. You cant just use this wherever.

You’re right of course that costs and benefits are somewhat subjective. As has been said, Go is an opinionated language.

That said I think that you are simply mistaken in the conclusions that you are drawing. I think it’s clear that Go has considerable tolerance for boilerplate. I don’t see how it’s possible to conclude that the Go team would only accept a proposal that reduces boilerplate down to zero lines.

I agree that additional syntax on the left hand side of an assignment statement is a bigger lift, since there is nothing like that in Go today. Currently the left hand side of an assignment must be addressable, be a map index expression, or be the special identifier _. In those three cases assignment is a fairly straightforward operation: just copy or discard a value. To change the left hand side of an assignment statement to cause some action other than assignment to occur is a big cost and requires a big benefit.

But to say that any of these ideas are ruled out a priori is to misunderstand the nature of costs and benefits. Even if costs and benefits are subjective, which to some extent they are, it doesn’t mean that the costs can not be incurred. It just means that they need a corresponding benefit.

Clearly there are some changes that will never be made to Go. For example, we will never break significant amounts of existing Go code; that is a cost that is so high that it’s impossible to imagine a corresponding benefit. But I don’t see why the kinds of changes we are talking about here rise to that level.

I could very well be wrong, but it seems to me that in these discussions you persistently underestimate the costs of a language change. That underestimation, if real, may lead you to conclude that the Go team must not see the benefits of the change. I encourage you to seriously consider that perhaps we do see at least some of the benefits, but that we judge the costs differently than you do.

Above you tried to separate cost to the spec, cost to the users, and cost to the tooling. That is very different from the way that I view the costs of language changes. The only cost that matters is the cost of understanding the language. Every special case in the language (and the language has far too many special cases already) is a cost. The goal is a language that is simple and orthogonal. Every concept in the language should ideally interact with every other concept clearly and simply. When certain kinds of expressions that can only be used in one place, when we add certain kinds of statement syntax that can only be used with one statement, those are big costs.

As you know, the discussion of the try proposal stressed that it was a builtin function that changed no other aspect of the language. That is another way of saying that cost of try was limited to understanding one more special case, a special case akin to the existing special cases of the builtin functions panic and recover. There was no new syntax, no new constraints, no affect on general language orthogonality. Those were important points in limiting the cost of try. Of course it still had costs, and the heated discussion was in itself a significant cost, and in the end we agreed that those costs were too high for the benefit of try.

Speaking purely personally, I don’t agree with @networkimprov 's comment at https://github.com/golang/go/issues/32611#issuecomment-512933966 . We don’t have specific requirements of that sort. If we did, we would say them.

We do our best to judge each proposal on its own merits. Every language change has costs and benefits. I want to stress, again, that this specific proposal introduced new concepts into the language, and that is a cost. We need a benefit that is worth that cost, and in the opinion of the proposal review team the benefit of this specific proposal was not worth the cost of this specific proposal.

To conclude from that that the Go team is only interested in “zero lines of boilerplate” or “no changes to assignment LHS” is to misunderstand what we are saying. We are weighing costs and benefits. We are not saying that only certain kinds of changes are acceptable. We are saying that the benefits of a change must outweigh the costs of a change.

It’s clear that the Go team was only interested in: a) zero lines of boilerplate (this proposal specifies 1 line), and b) no changes to assignment LHS (as in #32500).

This should be confirmed by the Go team and not merely assumed. (This is also what I was driving at with my set of scoping questions here.)

Hi @peterbourgon, some quick comments.

This current proposal is fairly broad, including the proposal itself has a fair number of permutations

Sometimes, “broader” is good because it means the solution is more general. In other cases, “broader” increases the cost of a change, or increases overlap with existing alternatives.

For example, as originally written, as far as I followed, any zero value is supported and it is not specific to error types, which would mean something like this is supported under the original proposal here:

someMap := getMap()
on someMap, goto label

In that case, that is a broader capability than strictly targeting error handling (which in turn requires a judgement call as to whether or not it is good to be broader in that way).

I think there is a narrower form of this proposal that is closer to what I think I might have seen you sketch roughly as:

a, err := f()
on err, errors.Annotatef(err, "message")

In any event, because this proposal has a fair number of permutations, in some cases it has been hard at least for me to understand which permutation a specific comment is addressing.

(Finally, I am speaking for myself as a more-or-less random gopher from the broader community. In other words, just to be explicit, I am not member of the core Go team).

@ohir Concerns around nesting is not new feedback, and again, a go/analysis check for nested trys or multiple trys on the same line seems entirely straightforward. And I suspect there would be support for getting it into vet as a default check.

@networkimprov I think this proposal needs to be specific about what exactly it’s proposing. The proposal seems to have a wide range of possible changes to the language, and it’s making it hard to evaluate. The specific keyword doesn’t really matter at all, but the exact semantics of what you’re proposing does. For instance, any thought like “is there implicit dataflow between an unnamed variable assignment and the following statement?” always has an answer of “maybe”, as there are some things in your proposal that imply yes and others that imply no. Same for a foundational question like “does the keyword only apply to values of type error?”.

It is the try’s ability to nest that concerns. Frankly, I know I personally would be tempted to abuse it, then curse on myself later.

This! It’s precisely the same temptation as the ternary operator proposals that the Go team has repeatedly quashed (rightly, imo.)

But there is really a difference between a built-in function and a new statement. Built-ins can easily be ignored (because they don’t interfere with existing code). With few exceptions (len, append), they can be skipped when learning the language, and introduced on demand. In the spec they show up at the end. Several built-ins are not strictly “needed” and one could write the same code by hand (append, copy, complex come to mind) - these are here purely for convenience. try falls into this category.

A statement cannot be ignored - it is either used or it must be avoided (in the sense that we cannot have identifiers that collide with keywords of statements). It must be learned by everybody for that reason. In the spec it shows up prominently.

@griesemer, that is a valid set of points.

That said, when it comes to learning about a try builtin vs. some statement-based variation, another set of questions is “how prominent would it be” and “how many words would it take to describe” in places like:

Setting aside for the moment the exact form a statement-based variation of try might take, one could certainly imagine that there is a statement-based variation of try that would end up being equally prominent in those places, and it could take roughly the same number of words to usefully introduce the concept and mechanism as a try builtin.

In other words, I suspect a builtin-based try would in practice end up being learned more-or-less just as soon for a new gopher as a statement-based try variation (given error handling is fairly central to how one writes Go), and one could imagine there exists a statement-based syntax that could be learned roughly as easily.

On your points about the spec – one of the things people praise about Go is how the spec is very readable, and seasoned gophers often recommend new gophers take the time to read the spec… but even there, a “typical” new gopher likely reads several other introductory things prior to venturing into the spec.

In any event, thanks for taking the time to share your thinking as well as to engage on the different concerns and ideas that have surfaced from the broader community since the proposal was published.

@ngrilly

The issue is that each time we query an external system, and this query can fail in multiple ways, we have 1 line to query the system, and 3 lines to check, decorate and return the error.

This on err proposal reduces it to one, which I think is the Pareto optimal number of lines, for the reasons I’ve discussed. Can not on err be the improved try?

Thanks, @peterbourgon for your detailed comment. We’ve picked up error checking because the community has repeatedly and strongly complained about the verbosity of error handling. And yes, we also think that it might be nice if there were a less verbose approach where possible. So we’re trying to move this forward if we can.

From what you’re saying, try would already be a big step up from checkErr - for one it takes care of dealing with an error in the first place, and it also promotes at least an error return rather than just a panic. Because it removes the need for a checkErr function and extra call on a separate line, I’d expect those checkErr programmers to flock to try.

I see your point that on err might be easier to change into proper error handling (i.e., decoration), but again, judging from the reality you are describing it seems that checkErr would still be the much easier way out. If these programmers have rarely enough time or experience to do error handling “properly”, why not provide a tool that gives them a strong incentive to check and forward errors? And rather than insist on decorating each error separately, use a deferred error handler (which can be one line)? This seems like a much easier sell than having them replace checkErr with an on err statement.

(As an aside, I’ve written plenty of little tools that just panic when I don’t care about the error - not great, but good enough when time is of the essence. try would make my life much easier and allow me to do something better than panic.)