1. 13
  1.  

  2. 7

    This is a very nice, succinct, and elucidating post. Well done.

    1. 2

      Am glad to see more posts on Go error handling techniques. Still, IMO, this approach seems to leave something to be desired, and IMO is still inferior even to this macro-based approach in C:

          1 OSStatus initKeychainAccess()
          2 {
          3     OSStatus err;
          4     DO_FAILABLE(err, SecKeychainSetUserInteractionAllowed, TRUE);
          5     DO_FAILABLE(err, SecKeychainUnlock, gKeychain, 0, NULL, FALSE);
          6     DO_FAILABLE(err, SecKeychainAddCallback, MyKeychainCallback, kSecEveryEventMask, NULL);
          7     doThatFancyThingYouDo();
          8 fail_label:
          9     return err;
         10 }
      

      That is simple, concise, and it doesn’t have the drawback mentioned here:

      There is one significant drawback to this approach, at least for some applications: there is no way to know how much of the processing completed before the error occurred. If that information is important, a more fine-grained approach is necessary. Often, though, an all-or-nothing check at the end is sufficient.

    2. 4

      What’s hard to ignore is that the correct code at the end does a lot of work to hide the fact that there is an error condition, and has to execute through the whole code regardless of if anything will be run. I think this is objectively worse than the Haskell or Ocaml, hypothetical code, that uses a monad. For example:

      write "foo
      >>= fun () ->
      write bar
      

      This code, depending on how >>= is defined, will bail out from the computation if an earlier one fails.

      I completely agree that errors are values. I think Go does it pretty poorly, not much better than C, and Ocaml and Haskell have figured this out awhile ago and have far superior APIs on top. Yes yes, it doesn’t matter because Go is very popular, but I just get ruffled by well-meaning-but-ignorant posts like these.

      1. 8

        well-meaning-but-ignorant posts

        There’s nothing ignorant about this post. It does a fair job at covering error handling in Go.

        I think this is objectively worse

        I’ve always wondered how that word can be used in the context of PL design. I think it is disingenuous. Notably, your comment ignores the costs of doing error handling that way. Compare, for example, Rust’s guide to error handling with this post for Go (or Pike’s original post). The difference is hard to miss.

        1. 5

          There’s nothing ignorant about this post. It does a fair job at covering error handling in Go.

          You’re right, I got lost in reading the article thinking it was about errors as values and Go as an example of that.

          I’ve always wondered how that word can be used in the context of PL design.

          The monadic method I mention above is superior to Go’s in every way i can see except for the part about it not being in Go. The reason for this is the Go solution given is a broken attempt at a monad. The Go solution requires zombie values to ignore future computation which it tries to do anyways, it is not very compositional, it requires always returning a product type when the actual abstraction is a sum type, and it does not force handling of errors which is explicitly cited as being important in the Pike article. The Ocaml/Haskell/Monad solution enforces all of these things. The programming has to explictly choose to not do them.

          1. 3

            The monadic method I mention above is superior to Go’s in every way i can see except for the part about it not being in Go.

            There is a cost: complexity. I submitted evidence to you in my previous comment.

            1. 3

              The link you gave is about Rust, which is not one of the languages I cited. It is also does not seem to be using monadic combinators as I am. It’s unclear to me what the complexity you are referring to is.

              1. 4

                The link you gave is about Rust, which is not one of the languages I cited.

                It is principally using the same style of error handling as Haskell.

                It is also does not seem to be using monadic combinators as I am.

                The similarities between try! and >>= are quite striking. And and_then is a concrete version of >>=.

                It’s unclear to me what the complexity you are referring to is.

                The article I linked to is over 10,000 words. This is not unique to Rust. The number of language features required to do monadic error handling is a reflection of this. Monadic error handling requires that the language at least have support for parametric polymorphism and sum types. Haskell in particular also benefits from higher kinded polymorphism and a programming model that encourages immutable data structures over mutable data structures. None of these things are present in Go. Adding them to the language, and forcing programmers to learn how to use them to handle errors, is a cost in complexity. You would also need to address the conversion of different error types, which is handled automatically in Go at the expense of somewhat increased complexity for callers that need to get at the concrete error (it requires a type assertion or type switch at runtime).

                Go’s error handling is simple. There is only one pattern you need to know:

                if err != nil {
                    return ...
                }
                

                That’s it. It is re-inforced again and again in exactly the same way. It is simple, in part, because it is simplistic. This means it is not privy to the long list of benefits you cited for monadic error handling. However, it does mean that it is simple, which is a benefit unto itself, both in the code it takes to write to handle errors and in the language as a whole.

                This notion that one PL design is “objectively” better than another is nauseating. Programming language design is about the balancing of trade offs. List the benefits, compare and contrast different approaches and let others make up their own mind about what is applicable to their particular situation. There’s no need for grandiose pronouncements that you happened to have stumbled upon the keys to what makes one design better than another in all possible cases. For example, some folks might reasonably attach a much higher value to the simplicity afforded by Go than you would. Who are you to say that they are wrong?

                1. 4

                  The similarities between try! and >>= are quite striking. And and_then is a concrete version of >>=.

                  Perhaps on the surface, but they are quite different underneath. try! just bails out whereas >>= allows for much more power. Yes, and_then is bind, however Rust does not appear to be expressive enough to make calling it not look painful. I would not use it to compare what I’m talking about.

                  The article I linked to is over 10,000 words. This is not unique to Rust.

                  As I said, I have not brought up Rust so I do not feel it is a useful comparision to what I have said.

                  Monadic error handling requires that the language at least have support for parametric polymorphism and sum types.

                  Indeed, but that is not an issue in Go because the compiler can support it like it does for everything else that requires parametric polymorphism. Much like how tuples are not a type but a specialization of assignment.

                  You would also need to address the conversion of different error types, which is handled automatically in Go at the expense of somewhat increased complexity for callers that need to get at the concrete error (it requires a type assertion or type switch at runtime).

                  This is no different in the suggested solution which chains a bunch of rights together.

                  Go’s error handling is simple.

                  I agree. That doesn’t make it good. @codahale gave an example of where this simple idiom can easily result in a bug. The error handling mechanism of Go also does not enforce that an error is handled, which was explicitly stated as being important.

                  This notion that one PL design is “objectively” better than another is nauseating.

                  My statement is that the Go model is inferior in every way to the monadic solution, including complexity and safety. In this sense, my statement of something being objectively worse is not a language trade-off issue. This is a technical statement. You can disagree and say my technical analysis is wrong, however this is different than claiming my statement is equivalent to opinion.

                  1. 2

                    Perhaps on the surface, but they are quite different underneath. try! just bails out whereas >>= allows for much more power.

                    Notice that I didn’t claim they were the same. How is that difference material to our current discussion?

                    however Rust does not appear to be expressive enough to make calling it not look painful

                    Indeed! Hence, try!.

                    Indeed, but that is not an issue in Go because the compiler can support it like it does for everything else that requires parametric polymorphism. Much like how tuples are not a type but a specialization of assignment.

                    Right, so, more language features. More complexity. More things the programmer needs to learn to do error handling. Which is exactly my point.

                    I agree. That doesn’t make it good. @codahale gave an example of where this simple idiom can easily result in a bug. The error handling mechanism of Go also does not enforce that an error is handled, which was explicitly stated as being important.

                    I never, ever once said “Go’s error handing is good.” I have also never once contested the downsides of Go’s error handling.

                    My statement is that the Go model is inferior in every way to the monadic solution, including complexity and safety.

                    This is false. It is more complex because it requires more abstraction and more language features. There are more things that the programmer needs to learn and master before one can effectively handle errors.

                    You can disagree and say my technical analysis is wrong, however this is different than claiming my statement is equivalent to opinion.

                    You are using the word objective, which implies you have knowledge of what makes a PL design better than another in every facet. I fundamentally disagree that this something that one can possibly know, and frankly, I find it extremely off-putting.

          2. 4

            I think allowing four possible outcomes, despite only two of them being meaningful or even expected 90% of the time could count very well as objectively worse.

            1. 1

              This is like saying that statically typed languages are objectively superior to unityped languages because there is really no end to the number of unmeaningful things one can express in a unityped language that would be disallowed in an expressive type system.

              There are plenty of legitimate uses of unityped languages, or, in this case, simplistic error handling. Namely, when one wants to avoid the cost in complexity of a safer variant that does very well at making sure the programmer writes correct code. (This can be from the perspective of the programmer or from the PL designer!)

              1. 4

                This is like saying that statically typed languages are objectively superior to unityped languages

                But a statically typed langauge is objectively superior to an unityped language because I can express everything about a unityped language in an statically typed language with the same runtime guarantees. It’s a mathematical fact.

                1. 1

                  There are a cornucopia of trade offs involved in choosing between a unityped language and a statically typed language. Ignoring these and appealing to “mathematical fact” is downright dishonest.

                  1. 4

                    That is not what my response said. A unityped langauge is a strict subset of a statically typed langauge, just with everything being some ‘dynamic’ type. I can therefore great a language that is statically typed but lets me do the semantic equivalent of Python. The is what languages like TypeScript do. So a statically typed language with this dynamic type is always superior to a unityped langauge because I can express everything the unityped language can, and more.

                    1. 1

                      is always superior

                      That doesn’t make any sense. You’re ignoring everything else about the language. Remember what I said:

                      This is like saying that statically typed languages are objectively superior to unityped languages

                      If you believe this to be true, then one can conclude that Haskell is “objectively superior” to Python. What does that even mean? It’s nonsense.

                      You say this:

                      So a statically typed language with this dynamic type is always superior to a unityped langauge because I can express everything the unityped language can, and more.

                      I disagree. I do agree with this however:

                      I can express everything the unityped language can, and more, with a statically typed language.

                      Do you see the difference? The former has your own little prescriptions buried in it. The latter is a technical claim that can be verified as true or false.

                      1. 1

                        That doesn’t make any sense. You’re ignoring everything else about the language. Remember what I said:

                        You’re right! I misinterpreted what you said! My apologies.

          3. 4

            depending on how >>= is defined

            I think this highlights a strength of how go does it: whenever an error is returned, there’s three possibilities:

            1. unhandled (none of the return values are assigned)
            2. ignored (assigned to _)
            3. dealt with

            All of these things happen explicitly in the code, I don’t need to figure out if an operator means something different or follow a different logic path.

            1. 12

              But because those things happen explicitly in the code, you’re constantly at risk of introducing defects in your error handling code. I’ve written a lot of Go and about 95% of the error handling code I’ve written has been some form of this:

              err := blah()
              if err != nil {
                return err
              }
              

              The end result is a codebase littered with bits of low-value code, often with poor test coverage (either due to the code being seen as low-value or because the error conditions are hard to simulate). But what could go wrong, you say?

              // forgetting to pass the error
              err := blah()
              if err != nil {
                return nil
              }
              
              // passing the wrong error
              if e := blah(); e != nil {
                return err
              }
              
              // accidental exit
              if err = blah(); err == nil {
                return err
              }
              

              All these are mistakes that I’ve personally made (usually caught by tests, often not) or seen in actual, no-shit codebases because humans have a very predictable tendency to zone out when doing boring, repetitive tasks like banging out error handling boilerplate.

              The virtue of higher level compositional techniques (like monadic errors) is that the error handling code is written once, tested exhaustively, and reused everywhere. A Haskell programmer isn’t at the mercy of arbitrary monad definitions any more than a Go programmer is subject to capricious Reader implementations — >>= with an Either is early exit, just like Read returns a non-nil error if something went wrong.

              I hear what you’re saying about Go surfacing where errors might occur, and that’s something I also appreciate about it. I just wish it had more options for composition to reduce the risk in handling those errors.

              puts another quarter in the “Programming Language Wishes Jar”

              1. 5

                All of these things happen explicitly in the code, I don’t need to figure out if an operator means something different or follow a different logic path.

                Given the solution described in the post here, how do you not have to do this? A >>= operator can be implemented just as close to the code that uses it as the solution described in this code. In general, though, one does not need to implement >>= because the existing ones do the Right Thing.

                unhandled (none of the return values are assigned)

                The solution in the article does not require this at all. The write calls could be done and not checked the error value in the end, the error has actually become implicit and easily unhandled. On top of that, not assigning return values does not explicitly mean it was ignored. It could be the API changed from returning nothing to something, so this is not explicit either.