Error checking in Go: The `try` keyword

288 views
Skip to first unread message

addi...@gmail.com

unread,
Feb 6, 2020, 10:28:24 PM2/6/20
to golang-nuts

Error checking in Go: The try keyword.

This isn’t a complete proposal it only describes basic idea and the changes suggested.
The following are modifications that deals with some of the problems introduced in the original proposal.

(I apologize if something very similar that uses a simple method to deal with adding context has been posted before but I could not find it.)

First, try is a keyword not a function builtin.

Here’s how error handling with the try keyword works:

try err

is equivalent to:

if err != nil {
    return err
} 

That’s it.

Example:

f, err := os.Open("file.dat")
try err
defer f.Close()

f.WriteString("...")

But, how to add context to errors?

 because try only returns if err != nil.
You can create a function that returns nil if an error is nil.

In fact, a function like this already exists in the errors package here.
https://github.com/pkg/errors/blob/master/errors.go

// Wrap returns an error annotating err with a stack trace
// at the point Wrap is called, and the supplied message.
// If err is nil, Wrap returns nil.
func Wrap(err error, message string) error {
    if err == nil {
        return nil
    }
    err = &withMessage{
        cause: err,
        msg:   message,
    }
    return &withStack{
        err,
        callers(),
    }
}

Example using it with try

f, err := os.Open("file.dat")
try errors.Wrap(err, "couldn't open file")
defer f.Close()

f.WriteString("...")

That’s it.

This should reduce repetitiveness with error handling in most cases.

It is very simple to understand, and it feels like Go.

haskell...@yahoo.de

unread,
Feb 6, 2020, 11:22:21 PM2/6/20
to golang-nuts
The original, rejected proposal was better because it was a built-in, not a new keyword, so it didn't break existing tools. Otherwise I don't see a difference.

MUNGAI

unread,
Feb 7, 2020, 12:55:35 AM2/7/20
to golang-nuts
I agree,
Some of the proposals introduce more trouble than the problem they are solving. For example, the proposed try works only iff you are returning a single value of type error. If you have more return values what happens?

Michel Levieux

unread,
Feb 7, 2020, 3:57:01 AM2/7/20
to MUNGAI, golang-nuts
Hi,

I'd like to add that for this particular purpose, I find the keyword "try" quite inappropriate. It makes sense in other languages, either with a catch "counterpart" or not. But in those cases (at least those I'm aware of), you're not trying a value, which makes no sense to me. You try an action, a process, a set of computations, and those actions, processes or computations throw - return - something if they fail. So it makes sense in those cases to use "try". "Trying an error", on the other hand, clearly is just wanting to have the same keyword for the same - approximate? - purpose (I'm not saying it's wrong or right, I'm just saying).

I know that most of the suggested keywords in proposals are not *really* part of the proposal itself and most often are just used as a means of showing practical examples to the reader. I'm just stating my opinion here.
Try seems a bad choice for me here because it focuses the attention on a single - irrelevant - point where the real value of the proposal would lie somewhere else (which does not imply that the proposal has value or it has not, again, this is not my point).

Le ven. 7 févr. 2020 à 06:55, MUNGAI <mung...@gmail.com> a écrit :
I agree,
Some of the proposals introduce more trouble than the problem they are solving. For example, the proposed try works only iff you are returning a single value of type error. If you have more return values what happens?

--
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/8584ff0d-deb1-483a-b152-45639a4ce088%40googlegroups.com.

ffm...@web.de

unread,
Feb 7, 2020, 4:25:45 AM2/7/20
to golang-nuts
f, err := os.Open("file.dat")
try errors.Wrap(err, "couldn't open file")

You approach has some merits to it. In addition to that some error handling solution needs to provide a way how to figure out what error happened, e.g. openening a file could fail because it does not exist or because of missing read permissions. How do you figure that out in a elegent way without lengthy if-then-else blocks?

pboam...@gmail.com

unread,
Feb 7, 2020, 4:48:14 AM2/7/20
to golang-nuts
Hi,
as I recently mentioned in another thread, you can already use this error handling stye without language changes:

https://play.golang.org/p/nDnXxPXeb--

(The function is named "check", not "try".)
See error decoration at the bottom.

Kevin Chadwick

unread,
Feb 7, 2020, 5:29:44 AM2/7/20
to golang-nuts
On 2020-02-07 08:56, Michel Levieux wrote:
> I'd like to add that for this particular purpose, I find the keyword "try" quite
> inappropriate.

When I did "Systems Programming" they didn't even teach C itself but rather some
C library functions, which wasn't very helpful when I look back on it.

I worry that Go may become convoluted over time. I guess I am fairly new to Go
and with a C background but I still see methods and even error unwrapping as
needless complexity even though they are simple constructs. In C, I avoid
function pointers for security reasons and if the line isn't too long I shall
even avoid |= because it provides less clarity, so one liners with multiple
methods, irritate, rather than please me.

In Linux you have multiple bridge tools and IP tools and some say this is good
as people can choose but the older brctl with a man page that enabled me to do
what I needed quickly, is deprecated. Competition is good but it should compete
against the existing tool.

So having multiple tools might please everyone theoretically but that doesn't
mean that it isn't adding complexity or causing issues, and pointless learning
curves should carry weight in decisions.

Being able to return multiple things and an error and having the line logged for
you without even writing an error function as I would in C has been perfect and
a no-brainer.

I am just hoping that wanting to import something or using the std lib doesn't
mean you have to waste time learning another obfuscation (personally, I see
methods as unnecessary already).

addi t0t08

unread,
Feb 7, 2020, 7:22:07 AM2/7/20
to golang-nuts
In case of multiple return values, it works similar to the original proposal. This should work with multiple return values. As I stated, this isn't a complete proposal.

Magnus Kokk

unread,
Feb 7, 2020, 11:13:46 AM2/7/20
to golang-nuts

[sorry, posted a half post before]

You would want to have the error value at hand and do everything explicitly.
Using the try keyword really feels like going back to some other languages where you would push everything up and then handle
a bunch of errors/exceptions in a "all eggs in one basket" fashion. It's a strength of go to have such granular value and interface semantics
available for dealing with any kind of values.

Eric Johnson

unread,
Feb 7, 2020, 7:38:56 PM2/7/20
to golang-nuts
To make an attempt at articulating why the error handling in Go is valuable as-is, I note the following points:
  • It is explicit, not hidden
  • The default idiom highlights "error checking being done here" (Go developers learn to look for lines "if err != nil".)
  • It is possible to set breakpoints on the error case
  • Code coverage of unit tests reports when the error case has been exercised.
  • It has the full flexibility of identifying the failure scenario, not limited to err != nil (although that is the overwhelming use)
  • It has the full power and flexibility of the language for handling the error - it can be logged, returned, recovered from, sent to a channel, etc.
  • If wrapping my error extends to multiple lines (due to longer messages, number of parameters in the error, etc., that works using existing language constructs.
  • It supports arbitrary return statement complexity, including multiple return values.
  • It is straightforward to parse / analyze.
That said, it has some drawbacks, particularly in the most common cases.
  • Vertical space: Even in complex cases, the error handling inside the "if" block can be reduced to at most two statements. One line for "handling" the error, and a second line for returning it (or a modified / wrapped version of it). With two additional vertical lines of boilerplate, that's either a 100% or 200% increase in vertical space (the "if" statement, and the closing brace). This limits the amount of code that can fit on my screen, which constrains my ability to comprehend larger functions.
  • Verbosity: The moment we're know we're checking for errors, the "if err != nil {", the "return", and the "}" are usually just extra syntax.
  • Scanning complexity: What if the check is not err != nil, but err != NotFound? Is this an error case, or an expected case? The current construct hides that distinction. We have to look to the next line to see how it is handled to know that it is an error return.
  • When there are multiple returns on a function, the error handling must specify them all, even though the common case is to return a "zero" value.
For language design purposes, the most common scenario for handling an error is a single-line error return. When it comes to error handling, there are two paths to choosing a new solution:
  • Identify a solution that captures the 80-90% case of if err != nil { return ... }
  • Identify a solution that can be used for all error handling, where the existing if err != nil then becomes a legacy code base choice, because it does not use the new construct which more clearly conveys "error handling here".
For the benefit of the language, I would prefer a solution that fits the second category, not one that fits the first category. Due to the fact that this proposed solution does not address the arbitrary response complexity, it seems like it fits the first category. If there's a way to tweak the proposal so that it can be used for *every* error handling case, then that would be better.

Go has a long history of implementing orthogonal solutions that happen to combine well. It is possible that error handling is one of those places that could benefit from teasing this question apart a little more.

Eric

addi t0t08

unread,
Feb 8, 2020, 2:02:34 AM2/8/20
to golang-nuts
I see your point. I think it may be better to narrow the scope of what should be improved.

For the most part, I like the simplicity of error handling in Go, but I would very much like a less verbose
way to pass errors.

Technically we are not talking about handling the errors but we just want to pass them.


The keyword 'try' may be misleading if our focus is just a simple way to pass errors.
I think the keyword we are looking for is `pass`. Similarly, `pass` only needs to return if err != nil.

func writeSomething() error {
   f, err := os.Open("file.dat")
   pass err
   defer f.Close()

   f.WriteString("...")
}
---
passing a wrapped error(using the Wrap function above which returns nil if err is nil (not a special function) ):


func writeSomething() error {
   f, err := os.Open("file.dat")
   pass errors.Wrap(err, "couldn't open file")
   defer f.Close()

   f.WriteString("...")
}


The following points from your reply still/or should work with `pass` (modified):

    •  It is explicit, not hidden
    • The default idiom highlights "error is being passed here" (Go developers learn to look for lines "pass err".)
    • It is possible to set breakpoints (should work similar to a return statement)
    • Code coverage of unit tests reports when the error case has been exercised.
    • Support for Wrapping errors
    • Supports multiple returns with zero values.
    • It is straightforward to parse / analyze.

      The following drawbacks are no longer there:
      •  Vertical space: Even in complex cases, the error handling inside the "if" block can be reduced to at most two statements. One line for "handling" the error, and a second line for returning it (or a modified / wrapped version of it). With two additional vertical lines of boilerplate, that's either a 100% or 200% increase in vertical space (the "if" statement, and the closing brace). This limits the amount of code that can fit on my screen, which constrains my ability to comprehend larger functions.
      •  Verbosity: The moment we're know we're checking for errors, the "if err != nil {", the "return", and the "}" are usually just extra syntax.
      • Scanning complexity: What if the check is not err != nil, but err != NotFound? Is this an error case, or an expected case? The current construct hides that distinction. We have to look to the next line to see how it is handled to know that it is an error return.
      • When there are multiple returns on a function, the error handling must specify them all, even though the common case is to return a "zero" value.

      The following however is not within the scope for 'pass':

      • If you want to log errors, you're handling it. 'pass' is only for passing errors.
      • If you want to check for a specific error. 'pass' is only for passing errors.
      • If you have a more complicated case.  'pass' is only for passing errors.

      pass should work with 100% of the cases within its limited scope. 

      I would like to submit a proposal for 'pass', but I'm not sure if it's something that would be considered.

      Brian Candler

      unread,
      Feb 8, 2020, 4:14:31 AM2/8/20
      to golang-nuts
      On Saturday, 8 February 2020 07:02:34 UTC, addi t0t08 wrote:
      I think the keyword we are looking for is `pass`. Similarly, `pass` only needs to return if err != nil.

      func writeSomething() error {
         f, err := os.Open("file.dat")
         pass err
         defer f.Close()

         f.WriteString("...")
      }
      ---
      passing a wrapped error(using the Wrap function above which returns nil if err is nil (not a special function) ):


      func writeSomething() error {
         f, err := os.Open("file.dat")
         pass errors.Wrap(err, "couldn't open file")
         defer f.Close()

         f.WriteString("...")
      }


      In the second example, how does it know that "err" is the value to check against nil?  If I wrote

      pass foo.Foo(bar, baz, qux)

      which of bar, baz or qux is checked against nil?  Is it looking for an actual variable called "err" ?

      Secondly: it doesn't capture the common case of returning a new (and not wrapped) error.

      if err != nil {
          return DatabaseConnectionError
      }

      Wrapping errors isn't always a good idea: it exposes your implementation details.  The API of the services you consume becomes part of the public API that you expose to your clients, which clients will then depend on.  You can therefore not change your implementation to consume a different set of services.

      Also, I don't see how your proposal handles functions which return more values than just an error:

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

      "pass should work with 100% of the cases within its limited scope." - in other words, not all cases.  It belongs to Eric's category 1: "Identify a solution that captures the 80-90% case of if err != nil { return ... }".  I prefer the explicit if err != nil to this.

      addi t0t08

      unread,
      Feb 8, 2020, 5:33:03 AM2/8/20
      to golang-nuts
      No, 'pass' accepts an error type. in this case Foo function must return an error type otherwise that would be a compile error.

      You can return a new error without wrapping. Just create a function. Let's call it Foo  (you can call it anything). Also, the implementation of Foo doesn't matter if it returns an error then pass will return the error value, if nil it will be ignored.

      func Foo(err error, text string) error {
             if err == nil {
                    return nil
             }

             return errors.New(text)
      }


      to use it with pass:

      pass Foo(someErr, "an error message")


      if someErr is nil then Foo will return  nil and pass won't do anything.

      I already said, multiple return values are supported (the original proposal also handles this) otherwise what's the point?. You can do this:


      func doSomeTask() (int, bool, error) {
              f, err := os.Open(filename)
              pass err
              defer f.Close()
              
              ....
              return 10, true, nil
              
      }

      this isn't a complete proposal so it doesn't explain everything in detail but it should be pretty straightforward. 
       
       

      Kevin Chadwick

      unread,
      Feb 8, 2020, 6:16:47 AM2/8/20
      to golang-nuts

      Your arguments against the current system seem very weak to me.

      An editor can if you really care; fold on if err like it can on functions. I would be strongly against hiding critical code in my company though.

      For if err == nil or not return at all but log and try again etc. We now have multiple cases and less transparent code.

      Hiding this away for "legacy use" is creating bigger problems than you are solving. It is no longer transparent to newbies how to do this crucial thing.

      IMO The "try" keyword is a confusing choice, it does not read well. Test might be better or if err !=nil; return syntax change even better but I don't see the point and can't imagine the ramifications.

      Having multiple ways of doing the exact same thing should be avoided, where possible.

      The only part that I see of use is handing callers() or stack trace to users on a plate, if it achieves that? OTOH the output might be more confusing....undecided.

      I wouldn't support this personally, not that I really count much. It doesn't seem to be solving a problem but creating them and rather solving a debatable preference.

      Brian Candler

      unread,
      Feb 8, 2020, 12:55:10 PM2/8/20
      to golang-nuts
      On Saturday, 8 February 2020 10:33:03 UTC, addi t0t08 wrote:
      No, 'pass' accepts an error type. in this case Foo function must return an error type otherwise that would be a compile error.


      Ah I see: you are relying on the behaviour of errors.Wrap(nil, "dontcare"), which is valid and returns nil.  That's a bit of hidden magic.


      I already said, multiple return values are supported (the original proposal also handles this) otherwise what's the point?. You can do this:


      func doSomeTask() (int, bool, error) {
              f, err := os.Open(filename)
              pass err
              defer f.Close()
              
              ....
              return 10, true, nil
              
      }


      For me, looking at this code I ask:

      - What would "pass" return for the int and bool arguments - the zero value for each perhaps?
      - What would it do instead if these were named return values?  Would it return the values already assigned, or always zero values?

      To me, this seems to be hidden magic, when compared with the original if statement, which has absolutely unambiguous semantics.

      Aside: I suppose it's worth mentioning that in principle golang lets you do this: 

      if f, err := os.Open(filename); err != nil {
          return 0, false, SomeError
      }

      However, this form isn't as useful as it first appears, because "f" drops out of scope after the "if" statement.  You can put the rest of your code inside an "else" block; or you can pre-declare your variables before the "if" statement.  Either way is more work than it saves.

      var f, g *os.File
      var err error
      if f, err = os.Open(file1); err != nil {
          return 0, false, SomeError
      }
      defer f.Close()
      if g, err = os.Open(file2); err != nil {
          return 0, false, SomeError
      }
      defer g.Close()
      ... etc

      addi t0t08

      unread,
      Feb 8, 2020, 1:38:19 PM2/8/20
      to golang-nuts
      On Saturday, February 8, 2020 at 10:55:10 AM UTC-7, Brian Candler wrote:
      On Saturday, 8 February 2020 10:33:03 UTC, addi t0t08 wrote:
      No, 'pass' accepts an error type. in this case Foo function must return an error type otherwise that would be a compile error.


      Ah I see: you are relying on the behaviour of errors.Wrap(nil, "dontcare"), which is valid and returns nil.  That's a bit of hidden magic.


      If you think about it, there is really no magic to it.

      pass nil


      doesn't do anything. 

      pass errors.New("some error")


      Works.


      Pass only cares about whats being evaluated. If there is an error, it will return it. Otherwise, nothing happens.
      To me, this is much more elegant than trying to add another keyword such as `handle` ... etc. 

      With pass, you can even add logging in the error wrapper function, stack traces ... etc. and should work fairly easily with existing error logging & handling libraries. 




      - What would "pass" return for the int and bool arguments - the zero value for each perhaps?
      - What would it do instead if these were named return values?  Would it return the values already assigned, or always zero values?

      To me, this seems to be hidden magic, when compared with the original if statement, which has absolutely unambiguous semantics.


      - It will return their zero values. 

      - Similar to proposed try built-in, they would keep whatever values they have.

       
      Aside: I suppose it's worth mentioning that in principle golang lets you do this: 

      if f, err := os.Open(filename); err != nil {
          return 0, false, SomeError
      }

      However, this form isn't as useful as it first appears, because "f" drops out of scope after the "if" statement.  You can put the rest of your code inside an "else" block; or you can pre-declare your variables before the "if" statement.  Either way is more work than it saves.

      var f, g *os.File
      var err error
      if f, err = os.Open(file1); err != nil {
          return 0, false, SomeError
      }
      defer f.Close()
      if g, err = os.Open(file2); err != nil {
          return 0, false, SomeError
      }
      defer g.Close()
      ... etc


      Yes but with pass, i think there would be no need to do it that way. 

      Also, without any additional changes to the language. Go already allows separating statements with semicolons, so it would be even possible to do the following.



      f, err := os.Open(file1) ; pass err
      defer f
      .Close()

      g
      , err := os.Open(file2) ; pass err
      defer g
      .Close()

      .....


      but I'm not sure if that would be a preferred style because it may not be possible to set breakpoints that way. 

      Even if its on a new line, it is a lot less code, and looks more readable to be honest.
       

      Brian Candler

      unread,
      Feb 8, 2020, 2:52:04 PM2/8/20
      to golang-nuts
      On Saturday, 8 February 2020 18:38:19 UTC, addi t0t08 wrote:

      Also, without any additional changes to the language. Go already allows separating statements with semicolons, so it would be even possible to do the following.



      f, err := os.Open(file1) ; pass err
      defer f
      .Close()

      g
      , err := os.Open(file2) ; pass err
      defer g
      .Close()

      .....



      gofmt will expand that onto multiple lines.

      I just don't like the magic treatment of error values.  For example, remember that error is an interface.  What if a function returns multiple values, but more than one of them happens to implement the error interface?  Which one will "pass" set?

      ISTM that what we have today is workable and unambiguous.  To make it tidier, one could propose something like Ruby's "if" statement modifier:

      f, err := os.Open(file)
      return nil, SomeError if err != nil

      but that has its own problems.

      Given that zero values are well defined, you could also add Python's ability to treat any type's non-zero value as "true":

      f, err := os.Open(file)
      return nil, SomeError if err

      I'm sure I read a post explaining why the go language designers rejected that, but I can't find it right now; only some opinion on stackoverflow.

      In any case: go is not ruby or python.  Every language has its own warts and wrinkles and special cases you need to learn - in go's case these include things like nil interface values versus nil pointers inside interface values.  But thankfully, go has a relatively low number of these.

      From that point of view, *not* introducing new syntax makes the language better - even if it means being a bit more verbose in what you write.

      I also found discussion at https://github.com/golang/go/issues/32825
      Reply all
      Reply to author
      Forward
      0 new messages