←back to thread

257 points pmig | 8 comments | | HN request time: 0.007s | source | bottom
Show context
blindriver ◴[] No.43096757[source]
I've been using Go for a while now. The biggest headache is error handling. I don't care what the "experts" say, having exception handling is so, so, so much cleaner in terms of error handling. Checking for err is simply bad, and trickling errors back up the call stack is one of the dumbest experiences in programming I've endured in multi-decades. If they are willing to add generics, they should add exception handling as well.
replies(4): >>43097025 #>>43097105 #>>43097710 #>>43098949 #
bmurphy1976 ◴[] No.43097105[source]
To each their own. I'm not going to claim to be an expert, but as somebody who's been coding since the 80s it was a breath of fresh air to see Go do what I wanted languages to do all long instead of ramming exceptions down my throat. I have problems with Go (examples: slice behavior and nil type interfaces) but error handling is not one of them.
replies(1): >>43097199 #
CharlieDigital ◴[] No.43097199[source]
What challenge did you run into with exception handling?

I'm curious because I've never felt it being onerous nor felt like there was much friction. Perhaps because I've primarily built web applications and web APIs, it's very common to simply let the exception bubble up to global middleware and handle it at a single point (log, wrap/transform). Then most of the code doesn't really care about exceptions.

The only case where I might add explicit exception handling probably falls into a handful of use cases when there is a desire to a) retry, b) log some local data at the site of failure, c) perform earlier transform before rethrowing up, d) some cleanup, e) discard/ignore it because the exception doesn't matter.

replies(6): >>43097659 #>>43098169 #>>43098430 #>>43099302 #>>43100129 #>>43101696 #
1. hedora ◴[] No.43098430[source]
Exceptions are fine if you never catch them. So is calling abort(). (Which is the Unix way to do what you described.)

If you need to handle errors, you quickly get into extremely complicated control flow that you now have to test:

   // all functions can throw, return nil or not.
   // All require cleanup.
   try {
      a = f();
      b = a.g();
   } catch(e) {
      c = h();
   } finally {
      if a cleanup_a() // can throw 
      if b cleanup_b() // null check doesn’t suffice…
      if c cleanup_c()
   }
Try mapping all the paths through that mess. It’s 6 lines of code. 4 paths can get into the catch block. 8 can get to finally. Finally multiplies that out by some annoying factor.
replies(3): >>43098503 #>>43099540 #>>43100151 #
2. CharlieDigital ◴[] No.43098503[source]
Can you give the equivalent in Go?
replies(2): >>43099445 #>>43099650 #
3. predictionfutu ◴[] No.43099540[source]
bracket pattern

    def bracket[A, T](ctor: () -> A, next: (a: A) -> T): T =
        val a = ctor();
        try { return next(a) } finally { a.dispose() }
4. coder543 ◴[] No.43099650[source]
I recommend ignoring the other reply you just got. They are clearly building a bad faith argument to try to make Go look terrible while claiming to sing its praises. That is not at all how that would look in Go. The point being made was that the exception-based code has lots of hidden gotchas, and being more explicit makes the control flow more obvious.

Something like this:

    a, err := f()
    if err != nil {
        c, err := h()
        if err != nil {
            return fmt.Errorf("h failed: %w", err)
        }
        cleanupC(c)
        return fmt.Errorf("f failed: %w", err)
    }
    defer cleanupA(a)

    b, err := a.g()
    if err != nil {
        c, err := h()
        if err != nil {
            return fmt.Errorf("h failed: %w", err)
        }
        cleanupC(c)
        return fmt.Errorf("a.g failed: %w", err)
    }
    defer cleanupB(b)

    // the rest of the function continues after here
It’s not crazy.

With Java’s checked exceptions, you at least have the compiler helping you to know (most of) what needs to be handled, compared to languages that just expect you to find out what exceptions explode through guess and check… but some would argue that you should only handle the happy path and let the entire handler die when something goes wrong.

I generally prefer the control flow that languages like Rust, Go, and Swift use.

Errors are rarely exceptional. Why should we use exceptions to handle all errors? Most errors are extremely expected, the same as any other value.

I’m somewhat sympathetic to the Erlang/Elixir philosophy of “let it crash”, where you have virtually no error handling in most of your code (from what I understand), but it is a very different set of trade offs.

replies(1): >>43099658 #
5. coder543 ◴[] No.43099658{3}[source]
Or, if you really hate duplication, you could optionally do something like this, where you extract the common error handling into a closure:

    handleError := func(origErr error, context string) error {
        c, err := h()
        if err != nil {
            return fmt.Errorf("%s: h failed: %w", context, err)
        }
        cleanupC(c)
        return fmt.Errorf("%s: %w", context, origErr)
    }

    a, err := f()
    if err != nil {
        return handleError(err, "f failed")
    }
    defer cleanupA(a)

    b, err := a.g()
    if err != nil {
        return handleError(err, "a.g failed")
    }
    defer cleanupB(b)

    // the rest of the function continues after here
6. never_inline ◴[] No.43100151[source]
> So is calling abort(). (Which is the Unix way to do what you described.

But in gui applications and servers, you will need to catch and report the error in some intermediate boundary, not exit the application. That's where go falls short.

7. CharlieDigital ◴[] No.43100880{3}[source]
This is satire, right?
replies(1): >>43108274 #
8. ted_dunning ◴[] No.43108274{4}[source]
No.

It is just an explicit rendition of a complex topic.

Do you have a more economical example that handles all of the corner cases of cleanup routines throwing errors?