Hacker News new | ask | show | jobs
by elvlysh 449 days ago
> Do this call

value, err := function()

> if there is an error return it

if err != nil { return err }

> otherwise give me the value

// rest of the code goes here

2 comments

And then you forget to write the err check once out of 100 times you have to write this verbiage. And the compiler lets you, because err was already checked in that same function (but for a different call), so it's not unused anymore.
Yeah, not a huge fan of error handling in go - stuck relying on a linter to catch you and because of shadowing rules it's extremely difficult to make it look nice.

Rust's `?` operator on Result<T,E> types is flipping fantastic, puts all of the following to shame.

    // can forget to check err
    thing, err := getThing()
    if err != nil {
      panic(err)
    }

    // More verbose, now you could possible forget to assign thing
    var thing Thing
    if t, err := getThing(); err != nil {
        panic(err)
    } else {
      thing = t
    }

    // What I end up doing half the time when I've got a string of many
    // calls that may return err as a result of this

    var whatIActuallyWant string
    if first, err := getFirst(); err != nil {
      return err
    } else if second, err := doWith(first); err != nil {
      return err
    } else if final, err := doFinally(second); err != nil {
      return err
    } else {
      whatIActuallyWant = final
    }
It's actually to the point that in quite a few projects I've worked on I've added this:

   func [T] must(value T, err error) T {
     if err != nil {
       panic(err)
     } else {
       return value
     }
   }
> stuck relying on a linter to catch you

Isn't that what your tests are for? Linters aren't normally intended to stop you from creating undefined behaviour.

It is not like Rust negates the need for those tests. Remembering to handle an error is not sufficient. You also need to ensure that you handle it correctly and define a contract to ensure that the intent is documented for human consumption and remains handled correctly as changes are made. Rust is very much a language designed around testing like every other popular language.

Relying on (someone making) a test to ensure you use a variable is even worse than relying on a linter.
You are absolutely right. But why would anyone do that? That doesn't make sense and it is bizarre that you would even put in the time to post this.

What you do need to do is document how the function is intended to behave. If, for example, your function opens a file, you need to describe to other developers what is expected to happen when the file cannot be open.

"The compiler won't let me forget to handle the error" is not sufficient to answer that. That you need to handle the error is a reasonable assumption, but upon error... Should it return a subsequent error? Should it try to open a file on another device? Should it fall back to using a network resource? That is what you need to answer.

And tests are the way to answer it. It is quite straightforward to do so: You write a test that sees the file open failure occur and check that the expected result happened (it returned the right error, it returned the right result from the network resource, etc.). Other programmers can then read your example to understand what is expected of the function. This is as necessary in Rust as it is in Go as it is in any other language you are conceivably going to be using. Otherwise, once you are gone, how will anyone ever know what it is supposed to do? As changes occur through the ongoing development cycle, how will they ever ensure that they haven't broken away from your original intent?

So, once you've written the necessary tests – those that are equally necessary in Rust as in any other language – how, exactly, are you going to forget to handle the error? You can't! It's impossible.

I don't know why this silly thought persists. It is so painfully contrived. If one is a complete dummy who doesn't understand the software development process perhaps they can go out of their way to make it a problem, but if one is that much of dummy they won't be able to grasp the complexities of Rust anyway, so...

You can do something like this:

   type errHandler struct {
      err error
   }

   func (eh *errHandler) getFirst() string {
      // stuff
      if err { eh.err = err }
      return result
   }

   func (eh *errHandler) doWith(input string) string {
      if eh.err != nil {
         return ""
      }
      //stuff
      if err { eh.err = err }
      return result
   }

   func (eh *errHandler) doFinally(input string) string {
      if eh.err != nil {
         return ""
      }
      //stuff
      if err { eh.err = err }
      return result
   }

   func (eh *errHandler) Err() error {
      return eh.err
   }

   func main() {
      eh := &errHandler{}
 
      first := eh.getFirst()
      second := eh.doWith(first)
      final := eh.doFinally(second)

      if err := eh.Err(); err != nil {
         panic(err)
      }
   }
You may as well use exception handlers if you're going to go there.

   func foo() (final int, err error) {
      defer func() {
         if e, ok := recover().(failure); ok {
            err = e
         } else {
            panic(e)
         }
      }()
      first := getFirst()
      doWith(first)
      final = doFinally()
      return
   }
encoding/json does it. It's okay if you understand the tradeoffs.

But look at what you could have wrote:

   func foo() (int, error) {
      first, err := getFirst()
      if err != nil {
         return 0, ErrFirst
      }

      err = doWith(first)
      if err != nil {
         return 0, ErrDo
      }


      final, err := doFinally()
      if err != nil {
         return 0, ErrFinally
      }

      return final, nil
   }
This one is actually quite nice to read, unlike the others, and provides a better experience for the caller too – which is arguably more important than all other attributes.
And with some error utilities you could do this:

  func foo() (int, error) {
      first := getFirst()?
      doWith(first)?
      return doFinally()
  }
or this:

  func foo() (int, error) {
      first := getFirst() % ErrFirst
      doWith(first) % ErrDo
      return doFinally() % ErrFinally
  }
The first one is a significant upgrade over the exception version. It cuts out half the code and makes the early return points explicit.

I think something similar to the second one is also nice to read, and it gives the same improved experience to the caller as your suggestion.

> and it gives the same improved experience to the caller as your suggestion

Albeit a contrived suggestion for the sake of brevity. In the real world you are going to need to write something more like:

   first, err := getFirst()
   var err1 *fooError
   var err2 *barError
   switch {
   case errors.As(err, &err1):
      return nil, FirstError1{err1.Blah()}
   case errors.As(err, &err2):
      return nil, FirstError2{err2.Meh()}
   case errors.Is(err, io.EOF):
      return nil, EOF{}
   // ...
   case err != nil:
      return nil, FirstError{err}
   }
And that is where eyes start to gloss over. The trouble with errors is that they quickly explode exponentially. Programmers long to distill all possible errors into one logical operation to not have to actually think about all the cases, since that is hard and programmers are lazy, but that is not sufficient for a lot of programming problems.

The cutesy shortcuts like ? and % operators are fine for some classes of programming problems, to be sure, but there are numerous languages that are already designed for those classes of problems. Does Go even need to consider travelling into those spaces? In the original Go announcement it was made explicitly clear that it was designed for a very particular need and was never intended to be a general purpose programming language.

I'm certainly not the gatekeeper. If Go wants to move away from its roots and become the must-have language for the classes of problems where something like ? is a wonderful fit, so be it. But, from my point of view, putting energy into tackling the big problems is more interesting. There should be plenty of room for improvement in the above code without losing what it stands for. But that is going to require a lot more deep thought than I've seen put in and programmers are lazy, so...

> You may as well use exception handlers if you're going to go there.

I didn't know about this trick, thanks for sharing.

> You can do something like this

Do people actually do this? Is it included in the standard library? If not, should it be?

That's not compact.
Compactness is in the eye of your beholder. You might have the eyes of a spider, but I have the eyes of a large orc.
> Compactness is in the eye of your beholder.

Not really.

If I want to do foo().bar().baz() it expands to six lines.