Go: Proposal: Go 2: error handling by assignment, with named handlers

Created on 9 Jun 2019  路  11Comments  路  Source: golang/go

Background

The feedback wiki for the Go 2 Draft Design proposing check/handle saw the emergence of two recurring themes, documented here:
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring-themes

13 responses suggested a nil test triggered by assignment, to reduce if err != nil boilerplate:

f, ? := os.Open(...) // one of many syntaxes suggested

17 responses suggested named error handlers, to enable multiple error paths within a function:

func example(c net.Conn) error {
   a := check os.Open(...) ? openErr // one of many syntaxes suggested
   ...
   b := check os.Open(...) ? openErr
   ...
   check io.Copy(c, a) ? copyErr
   check io.Copy(c, b) ? copyErr
   ...
   handle openErr { ... } // log error
   handle copyErr { ... } // log error, write error msg to c
   return nil
}

Others have newly reiterated both themes in comments on the try() proposal #32437.

Herein is a composite of these recurring themes. I believe it bears consideration in light of the apparently deep resistance to try().

Summary

v, _flag [op]_ := f()

try v, _flag [op]_ := f() // alternative, to indicate control-flow within statement

  • flag indicates that a zero-value test shall be performed on that return value,
  • op gives an optional procedure to invoke on a non-zero value,
    e.g. return, panic, or a named handler (defined at package-level or in current function)
  • try is a new keyword
  • v is an ordinary variable
  • := is any declaration or assignment operator
  • func f() (int, error)

Possible syntax (choose one)

   f, #return  := os.Open(...)
   f, ?return  := os.Open(...)
   f, @return  := os.Open(...)
   f, {return} := os.Open(...)

   try f, #  := os.Open(...)
   try f, ?  := os.Open(...)
   try f, @  := os.Open(...)
   try f, {} := os.Open(...)

Possible ops

   f, #return := os.Open(...)
   f, #panic  := os.Open(...)
   f, #hname  := os.Open(...) // invoke user-defined handler
   f, #       := os.Open(...) // ignore, or invoke "ignored" handler if defined

   try f, #      := os.Open(...) // return
   try f, #panic := os.Open(...)
   try f, #hname := os.Open(...)
   try f, #_     := os.Open(...) // ignore

Example handlers

   handle fatal (err error) {
      debug.PrintStack()
      log.Fatal(err)
   }

   handle log (err error, clr caller) { 
      fmt.Fprintf(os.Stderr, "%s got %s, continuing\n", clr.name, err)
      // may return the parent function or not, to support recoverable errors
   }
   // caller is a handle-specific type with results of runtime.Caller()

A sub-expression syntax (which has caused complaint in the try() comments) is possible, but only recommended for _ignore_ or _panic_ ops:

f(os.Open#(...))       // ignore
f(os.Open#panic(...))
f(os.Open#p(...))      // any leading substring of panic?

f(#      os.Open(...)) // prefix alternative
f(#panic os.Open(...))

f(os.Open(...) #)      // postfix alternative
f(os.Open(...) #panic)

About that Pesky New Symbol

People seem to dislike #op or @op or ?op, so let's ponder an alternative.

We could define three new keywords (for ops _return, panic, invoke_). They constrain the return value tested to the last one. To date, try & must have been suggested for _return_ & _panic_, and we could employ check for _invoke_. One can already use _ for op _ignore_, although it looks like a bug, and blank-assigned values can't be logged for debugging.

try   f := os.Open(...)            // return

must  f := os.Open(...)            // panic

check f := os.Open(...) else hname // invoke hname

   f, _ := os.Open(...)            // wait, is that a bug?

I'd rather define one new keyword (or none) and see a symbol with an op that names its action.

Work in Progress

As the try() proposal may be adopted, I leave this proposal in an incomplete state. I may develop it further if the Go team expresses interest.

More possibilities are described in _Requirements to Consider for Go 2 Error Handling_, but most of them are not "recurring themes".

__Who's__ @networkimprov? Liam is the author of

FrozenDueToAge Go2 LanguageChange Proposal error-handling

Most helpful comment

If the designers proposed this, many would see it as a great advance; succinct yet noticeable :-)

I'm not sure that authorship is the reason that people aren't liking it, don't blame it on that when there haven't been any responses. Whether or not you mean it this way, it comes off as incredibly selfish to blame others for not liking something that you made. (Selfish may not be the right word but I couldn't figure out a better one).

Also, have you seen the +1 to -1 ratio, and the comments, on #32437? Just because designers propose something doesn't mean that people will like it.

These four [operators] were new to me

In terms of the operators you mentioned - the ... is pretty common (it's called a spread operator and many languages, including old ones, have it). I've seen _ have special meaning in languages as well, typically denoting "something people shouldn't care about". In Python and Javascript they are used to hide private variables (since they don't have concepts of private variables in objects (except new JS, although it's a relatively unknown feature)). In Python however it's used the exact same way as it's used in Go. IIRC C#, Rust, and other languages have a similar feature that uses _ the exact same way that Go does. However if someone isn't well versed in those languages they can definitely be confusing.

I'll admit that := is a bit weird, however. In some languages, := is used for assignment while = would be used for comparison (aka Go's ==). It's also used in mathematics sometimes. I'll admit it's pretty obscure though.

The cost of introducing a new symbol must be weighed against the benefit of cleaner error handling. (Note that a lot of gophers don't want cleaner error handling of any kind.)

But what I'm getting at is that Go code can typically be explained with relative ease because it uses very few obscure symbols, nearly everything is typed out. I don't need to explain to someone what something like f := os.Open#panic(...) means because everything in Go is written out. Explaining that statement would sound something like "well we call os.Open, and the # means that if the error, provided by os.Open, is not nil, then we do something. In this case, we panic". And that'd only get worse as we add complexity.

However, when we write out the full statement, it becomes quite obvious:

f, err := os.Open(...)
if err != nil {
    panic(err)
}

Now it reads as "we call os.Open, and panic if the error is not nil". No need to explain what weird symbols mean.

I am someone who thinks that error handling is fine as it is, so take my words with a grain of salt.

All 11 comments

My main issue with this (and the reason I think that this has so many 馃憥 s) is because it relies heavily on symbols. Very few things in Go rely on unconventional symbols. The only one that I think Go actually uses (I may be wrong here) is the <- operator for channels - IIRC all other symbols in Go are well-known features in other languages.

These four were new to me <- := ... _.

The cost of introducing a new symbol must be weighed against the benefit of cleaner error handling. (Note that a lot of gophers don't believe that eliminating if err != nil is a benefit.)

If the designers proposed this, many would see it as a great advance; succinct yet noticeable :-)

If the designers proposed this, many would see it as a great advance; succinct yet noticeable :-)

I'm not sure that authorship is the reason that people aren't liking it, don't blame it on that when there haven't been any responses. Whether or not you mean it this way, it comes off as incredibly selfish to blame others for not liking something that you made. (Selfish may not be the right word but I couldn't figure out a better one).

Also, have you seen the +1 to -1 ratio, and the comments, on #32437? Just because designers propose something doesn't mean that people will like it.

These four [operators] were new to me

In terms of the operators you mentioned - the ... is pretty common (it's called a spread operator and many languages, including old ones, have it). I've seen _ have special meaning in languages as well, typically denoting "something people shouldn't care about". In Python and Javascript they are used to hide private variables (since they don't have concepts of private variables in objects (except new JS, although it's a relatively unknown feature)). In Python however it's used the exact same way as it's used in Go. IIRC C#, Rust, and other languages have a similar feature that uses _ the exact same way that Go does. However if someone isn't well versed in those languages they can definitely be confusing.

I'll admit that := is a bit weird, however. In some languages, := is used for assignment while = would be used for comparison (aka Go's ==). It's also used in mathematics sometimes. I'll admit it's pretty obscure though.

The cost of introducing a new symbol must be weighed against the benefit of cleaner error handling. (Note that a lot of gophers don't want cleaner error handling of any kind.)

But what I'm getting at is that Go code can typically be explained with relative ease because it uses very few obscure symbols, nearly everything is typed out. I don't need to explain to someone what something like f := os.Open#panic(...) means because everything in Go is written out. Explaining that statement would sound something like "well we call os.Open, and the # means that if the error, provided by os.Open, is not nil, then we do something. In this case, we panic". And that'd only get worse as we add complexity.

However, when we write out the full statement, it becomes quite obvious:

f, err := os.Open(...)
if err != nil {
    panic(err)
}

Now it reads as "we call os.Open, and panic if the error is not nil". No need to explain what weird symbols mean.

I am someone who thinks that error handling is fine as it is, so take my words with a grain of salt.

Gosh, I wasn't blaming anything on anyone. I was alluding to the high esteem the community has for the designers!

IIRC, the early thumb count on try() was >50% up.

Good to hear haha. Comments over text always come off differently as how they are meant.

Added above: About that Pesky New Symbol

People seem to dislike #op or @op or ?op, so let's ponder an alternative.

We could define three new keywords (for ops _return, panic, invoke_). They constrain the return value tested to the last one. To date, try & must have been suggested for _return_ & _panic_, and we could employ check for _invoke_. One can already use _ for op _ignore_, although it looks like a bug, and blank-assigned values can't be logged for debugging.

try   f := os.Open(...)            // return
must  f := os.Open(...)            // panic
check f := os.Open(...) else hname // invoke hname
   f, _ := os.Open(...)            // wait, is that a bug?

I'd rather define one new keyword (or none) and see a symbol with an op that names its action.

Dean, this is not addressed to you; please ignore it :-)

Hey hey hey, no need to make it personal haha. I actually quite like it. I know there are a lot of compatibility arguments against something like that, but it really does look nice IMO. I sorta liked solutions like that in the other error handling proposal too.

Having a special syntax to avoid explicit and in place error handling is IMHO a more way to postpone the problem, a more way to increase tech debt.

this proposal suggests a Perlish style for Go and makes it tricky to work!

I don't think I like this proposal, but I don't see why it needs a special character. You could just rely on the fact that return and goto are already keywords, and on the fact that you can't put a function on the left hand side of an assignment.

    x, return := F()
    x, goto handler := F()
    x, panic() := F()

Of course it would be necessary to define what values are returned for the return case, and we would have to provide some way to access the error returned by F in the handler. And it would be nice to use that in the panic call. And then we could even write

    x, return fmt.Errorf("my error: %v", err) := F()

One approach would be to use named result parameters, although that confuses the error result of F with the error result of the caller.

All that said I think it's unlikely that we would adopt this. It seems too ad hoc for the problem that it solves.

Speaking only for myself.

This proposal creates a sort of special assignment destination: if you assign to one of these special destinations, then something different happens. We would have to understand how this works in package scope. We would have to understand the order of operations. Presumably the special destination can be used anywhere on the left hand side of an assignment, so what happens with

    #return, #return := F()

or even

    #return, #return := F1(), F2()
    #H1(), #H2() = F3()

In short this seems to require quite a few more rules regarding evaluation order.

We only have a few special characters left unused. Is this special purpose operation the right way to use one of them?

This could probably be made to work, but it comes at a significant cost in complexity of assignment statements. Also the test against a zero value seems somewhat ad hoc, and seems to be the opposite of what one might want for non-interface values such as booleans.

There doesn't seem to be much enthusiasm for this proposal. It's not clear that people want this kind of shortcut in the first place, as we've learned from other proposals in this space.

-- writing for @golang/proposal-review

Was this page helpful?
0 / 5 - 0 ratings

Related issues

natefinch picture natefinch  路  3Comments

jayhuang75 picture jayhuang75  路  3Comments

longzhizhi picture longzhizhi  路  3Comments

stub42 picture stub42  路  3Comments

ajstarks picture ajstarks  路  3Comments