Hacker News new | ask | show | jobs
by elvlysh 445 days ago
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)
      }
   }
2 comments

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...

If you need different logic for different errors out of a function call you wouldn't use this, but your example code there... I think it's at the point where you've made things more complicated for your caller than just returning FirstError{err} no matter what the error is. The caller still has to deal with all the errors getFirst can cause, but they've been reorganized in a complicated bespoke way.

> Does Go even need to consider travelling into those spaces?

Oh come on. Changing how one common piece of boilerplate is written is not travelling into new spaces or moving away from Go's roots.

> 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?