1. 8
  1.  

  2. 42

    Why are these kinds of articles almost always predicated on the authors being completely unaware of Option/Result types?

    It’s the old “beating down the exception strawman” – that no one is even interested in defending.

    (I wonder what happens to all these similar articles when Go gets generics and people start using Option/Result types?)

    1. 16

      The author does give lip service to Option/Result types:

      EDIT: Exceptions are definitely not the only way other languages deal with errors. Rust, for example, has a good compromise of using option types and pattern matching to find error conditions, leveraging some nice syntactic sugar to achieve similar results.

      But in a way that suggests they’re something he’s heard of, rather than personally used. If you’re already used to working with ADTs that contain errors, I can’t imagine finding Go’s system of basically mandatory 2-tuple return types for all error-prone functions to be ergonomic.

      1. 12

        Agreed. Plus all the fun in figuring out whether you actually need to handle the value and error case, because the signature doesn’t tell.

      2. 8

        As someone who was an adamant gopher for like 7 years. Rust’s Option and Result types changed the way I see programming, especially once I fully grokked that Result is basically a monad.

        1. 4

          Same, although I had that realization when encountering the equivalents of those types in Haskell, rather than Rust. A lot of what I like about Rust is that it’s a mainstream language (more mainstream than Haskell in any case) that lets you easily define and use ADTs.

        2. 11

          It’s the old “beating down the exception strawman” – that no one is even interested in defending.

          I dunno, having used Go as my main development language for eight years now, I encounter people defending exceptions far, far more often than people advocating for option types.

          1. 5

            Isn’t it that, at this point in time, most people familiar with option types for errors are developing in Rust and not really in the Go world anymore ?

            Somebody coming from a language with exceptions to Go might say he prefers his old way. And there probably aren’t, today, many people coming from Rust to Go.

            1. 6

              sure, I think there aren’t many people coming from Rust to Go, that’s … kinda my point? soc is mad that the article is not written specifically for him, and is instead written for an audience that is more representative of who the author is actually encountering. The idea that exceptions are a straw-man nobody is defending is … honestly a little ridiculous. People defend exceptions all the time. The majority of programmers work in JS, Python, Java, C#, and PHP, all of which have error-handling semantics built around exceptions. A lot of those programmers really do make arguments defending exceptions. Few of them read lobste.rs for recreation. And you know, the few that DO read lobste.rs for recreation need articles like this to learn about things they haven’t encountered yet.

              I also find it so hilarious that someone flagged my prior comment as “incorrect”. I described my lived experience and someone flagged it as “incorrect”. lol

              In reality, most people just code because it’s a job. This article is not written for people looking to find self-actualization in a type system.

              1. 3

                I also find it so hilarious that someone flagged my prior comment as “incorrect”. I described my lived experience and someone flagged it as “incorrect”. lol

                Looks like all my comments on this thread have received at least one flag.

                I don’t know what it is about Go that induces such rage in some people, it’s completely fine (even reasonable!) to not like Go, but Go threads have a tendency to turn in to dumpster fires in a way that I don’t see in other languages (including PHP and JavaScript).

                1. 1

                  I don’t know what it is about Go that induces such rage in some people

                  classism.

            2. -10

              I’d assume that people with higher language quality standards usually don’t intermingle with Go devs.

              Communicating with Go devs certainly forced me to reevaluate the minimal amount of knowledge I assumed to be absolutely necessary to be a functioning programmer.

              1. 15

                Communicating with Go devs certainly forced me to reevaluate the minimal amount of knowledge I assumed to be absolutely necessary to be a functioning programmer.

                Dude, what the fuck? This has no place here.

                1. -4

                  I don’t think you need to get emotional over this.

                  I reported my experience, just like other people have in this thread.

                  1. 1

                    I did not downvote your comment, because I did not get emotional over it. What happened here I think is that people read “Go devs” and instinctually took your rather laconic criticism of the technology to be criticism of the members (i.e., themselves) belonging to the said group.

                    It is easy to become a target of put-down by aggravated members of any group if you are not vigilant with your words.

                2. 5

                  “Enable fresh graduates to contribute effectively” was a core design goal for the language.

                  It’s not surprising, then, that there’s no shortage of beginners to be found there.

                  1. 4

                    In my experience/interactions there are actually comparatively few new programmings in Go; much of the documentation and resources also tend to be aimed more at people who already know how to program in another language, I’ve seen quite a few people complain about the lack of entry-level jobs, the community is not always very welcome to beginners, etc. Generally speaking, I wouldn’t recommend it as a first language.

                    This is just my subjective impression; I don’t have any numbers to back this up and could be wrong.

                  2. 5

                    The Rob Pike quote (which seems to have come from this talk: https://channel9.msdn.com/Events/Lang-NEXT/Lang-NEXT-2014/From-Parallel-to-Concurrent ) was:

                    “The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.”

                    And this left a bad taste in my mouth when I read it originally. Either Rob Pike is right about the sorts of people he needed to optimize for when creating Go, implying that Google hires fresh out of college aren’t skilled enough programmers to deal with things like ADTs; or Rob Pike was wrong about who he was optimizing for and made a language that deliberately limits the tools you have to write correct and concise software because he mistakenly thought those tools were too hard for people to use.

                    1. 5

                      Agreed.

                      It really feels like Go creators think nothing of value was invented/discovered since they left Bell labs.

                      Also, using one of their team members as the language mascot is kinda weird.

                      1. 5

                        Also, using one of their team members as the language mascot is kinda weird.

                        Huh? The Go Gopher was based on one of their team members?

                      2. 3

                        the fact that you find that quote upsetting is rooted in either class dysphoria or class chauvinism. One can only find that statement upsetting under one of the following two conditions:

                        • class chauvinism: you think that programming should be reserved for an intellectual elite, and that opening programming to the labor class is a form of class betrayal by an intellectual elite. What you reject is not the design or implementation of Go itself, but the goals that the Go team has laid out when attempting to create the language. That is, the frustration is with the very goal of creating a programming language that can be used by the unwashed masses because programming should be reserved for the privileged.

                        • class dysphoria: you reject the idea that the majority of software is written by a labor class, and that you are a member of that labor class. You are upset by the phrase “to use them to build good software”. This is only upsetting to you if you don’t accept your position that as a person selling their labor to make a living, your employer is interested in leveraging your labor to serve a business goal, and not because you are in some way special beyond the output of your labor. A person that digs holes for a living would not be bothered by the phrase “to use them to dig a hole”, because they understand the nature of the exchange being made; they understand that they are being used to accomplish somebody else’s goal in exchange for cash.

                        Both of these cases are rooted in hatred of the labor class, because both of these cases are rooted in the idea that members of a laboring class are without dignity. If you are a member of a labor class with an awareness of the fact that you are a member of the labor class and you feel solidarity with the laboring class, Go’s goals seem perfectly reasonable. I like Go because I think it helps me perform my role as a laborer creating software as part of a collective effort. As a manager and team lead, I enjoy that it makes onboarding programmers to a new codebase very easy, and that it makes it easy for my developers to write mutually-intelligible code.

                        If I were rich, I would not write Go for myself for enjoyment unless I was doing so expressly with the goal to turn what I was building into a business, and for the code to be passed on to future wage-earning programmers. Go is not, from my perspective as a person who has been writing Go since 2011, a great tool for writing beautiful, expressive poetry; it is a great tool for building information-processing machines that will actually be used.

                        Go is designed by a corporation to pursue the goals of that corporation. It says as much on the tin. It is hard to argue that Go is unsuccessful at its own goals of being a programming language with which a large labor class can construct stable software. Go is tremendously successful at fulfilling those goals, and its success causes one to deal with issues of socioeconomic class. If you have never thought about class, this can be disturbing, as it lays naked the reality of classism in society, and one’s place within that system.

                        1. 9

                          I don’t like Go because it was designed for the average programmer or a contempt of them, I don’t like it because it’s a paternalistic and reactionary language that assumes the average programmer is too stupid to handle after anything that wasn’t status quo in 1995.

                          The average programmer might not have a CS degree, but you don’t need a CS degree to have sum types and pattern matching make error handling less error prone, making their lives (and everyone else’s) easier. Go rejects this premise, because worse is better and Bell Labs got it right - dooming us all to deal with the sharp edges we keep getting cut on.

                          1. 0

                            “I’m too smart to program with something that dumb people can program with”

                            1. 9

                              It’s not a matter of elitism, it’s a matter of being a dumb person (I’ll admit this - a lot in CS makes my eyes glaze over, I see so many people that are smarter than me), I want smart tools to help me be a more productive programmer, instead of struggling with common mistakes or writing menial boilerplate. I hate that there’s so many new ideas on the horizon, but they remain out of touch in the distance to the average programmer. As an industry, we should be bringing making new ideas and tools more accessible, instead of worshipping what has been already done poorly.

                              I’m smart enough to know we could have things so much better, but not smart enough to do it by myself.

                              1. 3

                                I hate that there’s so many new ideas on the horizon, but they remain out of touch in the distance to the average programmer.

                                I dunno, that doesn’t really resonate with me all that much because I don’t feel like anyone is discouraging me from learning those things. You can use Rust or Haskell or Lisp or whatever, all of that stuff is freely available to you. Nobody is creating barriers to using or exploring those things.

                                As an industry, we should be bringing making new ideas and tools more accessible, instead of worshipping what has been already done poorly.

                                On the one hand, I sorta think that’s what Go does. Structural typing, CSP, and getting rid of exceptions are pretty major changes from the C++/Java/C# model. Go was designed specifically to deprecate C++ and Java at Google. Go is just supposed to be a better Java. It really seems to succeed at that.

                                On the other hand, I think you’ve twice conflated “new ideas” with “good ideas”. A lot of new ideas are actually bad ideas. Inheritance was a new idea once. Turns out it’s bad. Go isn’t trying to have a lot of ideas or a lot of new ideas.

                                As for sum types specifically (which is both a good idea and an old idea): if you look at the long arc of the language, one of the driving principles is orthogonality. Each part of the language is expected to have as little overlap as possible with the other existing parts of the language. There is a large surface area of overlap between sum types with pattern matching and interface types with type switching. I’m not saying those are the same things; they are not the same things. Add sum types, cool! They’re useful! But you now have two mechanisms for looking at a value and switching on its type that can be checked at compile time. The design philosophy of Go attempts to reduce instances where different aspects of the language have that much overlap. A major thing that pattern matching does is enforce that the match be exhaustive, and the fact that type switches aren’t exhaustive is a huge source of sneaky and annoying bugs. Type switches will never be exhaustive because, for example, the number of possible types implementing interface{} is innumerable. As it turns out, this is a huge bummer both in theory and in practice.

                                I’ve been programming Go since r59; before Go 1.0. The discussion about sum types has been around since then. The open proposal to add sum types to the language has many links to past discussions. https://github.com/golang/go/issues/19412

                                Practically speaking, there is exactly one place where I really wish I could say (T | error), and that’s when managing unreliable procedures over channels. If you have to fan out an operation to N goroutines, but that operation can fail such that you collect all the errors and all the values on a single channel, you wind up having to create an ad-hoc product type every time you do that for the channel, and that’s really damned annoying.

                                1. 5

                                  It’s sad that this kind of insightful comment is buried under a comment that’s flagged to invisibility…

                                  1. 2

                                    it also got flagged lol

                            2. 1

                              This sounds reasonable, and I completely understand this perspective.

                              As a counter-point, I rather like this approach as it allows me to focus on what I’m doing and not the language; I’ve rather taken a liking to the “dumb” approach in Go (Stockholm syndrome? Perhaps 🙃)

                              I’ve also programmed a lot with Ruby in the past, and even after several years there were still corner cases and surprises in the language. Don’t get me wrong, I still like Ruby (actually, I miss programming in Ruby, it’s a fun language) but that approach does come with some downsides. Perhaps Go will be “obsolete” in 10 or 20 years when newer languages such as Rust, Zig, or some other have matured and stabilized a bit more (although I’m not sure if I will ever truly like Rust, since some design elements don’t particularly jibe with me, but I’d have to program more in it to be sure).

                          2. 2

                            Go is a language designed for software engineering in the large. At scale, there are no good or bad programmers, there are only average programmers, by definition. So it’s appropriate for a language to cater to that invariant. If you’re not programming at scale this design decision makes less sense for you and that’s fine, one tool doesn’t need to be all things to all people.

                            1. 1

                              -1 incorrect

                              I’m pretty sure I’ve attracted my first Hater! Boggles the mind.

                            2. 1

                              What alternative do you think is worse?

                              I think the first one is the correct one, and Pike and the team that created Go knew exactly what kind of language that Google needed - seeing as they worked there, and presumably had talked to a number of stakeholders in the company.

                              “FizzBuzz” is a meme for a reason. A lot of people with CS degrees literally cannot program. Presumably Google is big enough and prestigious enough to filter those out, but that doesn’t mean the rest is automatically fluent in Haskell or something.

                            3. 4

                              yes all programming languages exist on a single dimension of performance called “quality”, that’s definitely how the industry works 👍

                              1. [Comment removed by moderator pushcx: Electoral politics is neither topical nor likely to improve this disagreement.]

                                1. 4

                                  that’s not at all what I said. I’m saying different languages are good for different things. The idea that there’s a language that’s just “the best” is, quite frankly, juvenile.

                          3. 6

                            It’s the old “beating down the exception strawman” – that no one is even interested in defending.

                            It’s not a straw man when a large percentage of competing languages actually do use exceptions. There are very few mainstream languages that use Option/Result types. Actually, I think Swift might be the only one? And maybe Rust, but I’d argue that’s probably not a mainstream language, not yet anyway.

                            1. 9

                              Isn’t it a problem in Go culture that it is content in comparing itself with mainstream languages from an earlier century instead of actively investigating what can be done in contemporary non-mainstream languages?

                              This attitude often leads to solving the wrong problem.

                              1. 2

                                That sounds about right. Go is not designed to break any new ground in computer science or language design, but rather to implement concepts that have been proven to work in various languages and are well understood. I think there is value in this: it gives a stable, reliable, and boring language, which may not be optimal in all respects, but it doesn’t have a lot of “sharp edges” and idiomatic Go code from 10 years ago is quite similar to Go code today. Especially if you want to write an application intended to be maintained for a long period of time (>10 years) then the Go approach can give you some good advantages, I think.

                                I think there’s a lot of value in the more experimental approach in e.g. Rust too, by the way. Not all languages need to be the same.

                                Either way, I personally wouldn’t call this a “problem”, just a design ethic.

                                1. 6

                                  That sounds about right. Go is not designed to break any new ground in computer science or language design, but rather to implement concepts that have been proven to work in various languages and are well understood.

                                  Haskell and OCaml have both been around since the 90s, and those are just the two most obvious examples I could come up with of languages that have been used a fair amount in production and which also provide ADTs. So I’m not sure how your assertion makes sense.

                                  1. 9

                                    Seconded. The “experimental” part of Rust is the borrow checker, the rest (a bit of type inference, sum types/enums, match) is extremely well known and robust stuff that has been around for > 40 years. Go ignores a lot of well known things, not just new ground.

                                    1. 1

                                      And both are also functional programming languages that most people struggle with and see quite little real-world use in the industry.

                                      1. 2

                                        Both languages are used extensively in production and have been for a long time, regardless of how their usage compares to C or Java or what-have-you. And ADTs are quite well understood at this point, and Maybe/Option types specifically even moreso; we’re not talking about these languages’ learning curves here. Maybe/Option has been introduced (badly) into Java, for example, and is present in Kotlin, Scala, Rust, etc. So I don’t see how your comment here contradicts my original point.

                                        1. 2

                                          We could argue at length on what is and isn’t “well understood” and “widely used”; but it’s not a binary thing, it’s a scale. And it’s also not completely objective either and there is quite some subjectivity involved.

                                          Go has a very conservative take, which certainly isn’t perfect and you do miss out on some things, but as mentioned I think there is value in this approach too. YMMV.

                                2. 0

                                  I don’t really feel like defending the 90ies poor error handling approach against a language from the 60ies. ¯\_(ツ)_/¯

                                  The people who care have moved on from such inferior approaches, and those who are still stuck in the last century can’t be helped anyway.

                              2. 6

                                Most linters or IDEs will catch that you’re ignoring an error

                                I’m not always using linters or IDEs. Does that mean that I should be able to just ignore the error? I don’t think so

                                Plan for failure, not success

                                The default behavior in Go assumes success. You have to explicitly plan for failure

                                has sufficient context as to what layers of the application went wrong. Instead of blowing up with an unreadable, cryptic stack trace

                                I don’t know how about you, but math: square root of negative number -15 doesn’t exactly tell my why the error occurred. I’d rather look into that “cryptic” stack trace to find out where that negative number got used.

                                1. 0

                                  math: square root of negative number -15

                                  math.Sqrt(-15) = NaN. I have no idea where you’ve seen that particular error message. Cases like that are exceptional and are a programmer’s mistake (e.g. division by zero), therefore Go would panic with a full stack trace.

                                  But let’s assume that there is some square root function that returns an error. We usually add annotations to error messages, e.g. in Go ≥1.13 fmt.Errorf("compute something: %w", err) and later check that errors.Is(err, math.ErrNegativeSqrt), so instead of

                                  math: square root of negative number -15
                                  

                                  you’d actually see

                                  solve equation: compute discriminant: square root of negative number -15
                                  

                                  which is way more descriptive than a stack trace if there are multiple similar error paths along the call stack (from my experience, that’s almost always true). With exceptions you’d have to carefully follow the stack trace line-by-line to find the cause of an error.

                                  Also, in many languages discovering that a particular function throws an exception without code analyzers is pretty hard.

                                  1. 5

                                    I have no idea where you’ve seen that particular error message.

                                    I just took it as an example.

                                    We usually add annotations to error messages

                                    Usually. But the default for go is just return err. Which leaves you with the problematic one.

                                    which is way more descriptive

                                    I don’t know how about you, but in a project with >10k LOC I’d like to see, in which exact file do you have to open for an error. I won’t always know the project by heart, and to find it normally would take a good couple of minutes.

                                    if there are multiple similar error paths along the call stack

                                    Uhhh, you just look into the first function along the call stack and go along in such case. It’s not that difficult. I know it can look scary, but stack traces usually have all the information that you need, and then some. Just learning their syntax will allow you to efficiently find causes of bugs. That cannot be said for Go, as different projects can use different styles of error annotations.

                                    With exceptions you’d have to carefully follow the stack trace line-by-line to find the cause of an error.

                                    I usually read less than half of it before realizing where the error lies. There is a ton of information in a call stack, and you don’t always need all of it.

                                    Also, in many languages discovering that a particular function throws an exception without code analyzers is pretty hard.

                                    In a simple case, looking for raise/throw/etc. is enough. But yes, this is a problem. Some languages do have a solution, e.g. Java, but those aren’t that common yet. I’d like improvement here.

                                    1. 3

                                      What happens if “solve equation” calls “compute discriminant” multiple times? There is no line number telling me where it was called.

                                      My preference is a good error message with a stack trace. I can choose to ignore the stack trace when looking at logs/output. Also, (in some languages) a stack trace can be generated without an exception.

                                      1. 1

                                        line numbers assume a few things:

                                        • you have the same version of the code everywhere. in a production environment, you can easily have multiple versions of the same thing during deployments, upgrades, etc. Those situations are the most common situations in which you would encounter an error for the first time since, as we just said, you’re deploying potentially new code.
                                        • you’re only talking about comprehending the error data as it happens, and not historically, in a log file somewhere. What if you’re looking at log data from a few days ago? Now you have to cross-reference your stack trace with what version of the app was running at the time that stack trace was generated. Ok, now check out the code at that time. Now step through the line numbers and run the program in your head to try to understand the story of how the error happened. -OR- just … read the error’s story of how it happened in a Go program.
                                        • a stack trace is generally not going to capture local variables. Even if you know what line you’re on, you might not know what parameters that function was called with. With explicit wrapping of error values, you append to the error value the necessary contextual information, as decided by a human author, for a human to understand the cause of the error.

                                        I don’t miss exceptions and stack traces after using Go for a few years. Both are, in my experience, actively worse than handling errors at the call site and wrapping errors with descriptive contextual information.

                                        1. 2

                                          I use both error context and stack traces; you can add stack traces to errors quite easily (about 20/30 lines of code) and it never hurts to have more information. In some cases there are performance problems with stack traces (an issue in any language), but those are rare enough that I’m not overly worried about it.

                                          Cases where error context fails for me is when I’ve accidentally added identical context to two errors. This is usually a mistake on my part (copy/paste, or just laziness) and having the stack trace is helpful. I also find it easier in development because I can just go to “foo.go line 20”.

                                          I replaced all pkg/errors calls with some sed-fu to Go 1.13 errors, but I added back stack traces after a few weeks with a small library (about 100 lines in total).

                                          Either way, I don’t think context and stack traces need to be exclusive, and there is value in both in different contexts.

                                  2. 4

                                    One of my favorite language design choices in the realm of thrown exceptions: A function in Swift that throws must be declared to throw, and when you call it you must write a try keyword in the calling expression. try itself doesn’t do anything, it’s just required as a clue to the reader: You can always tell which lines might throw and which will only return. (There is a do keyword which starts a block like most languages’ try.)

                                    1. 2

                                      One of my favorite language design choices in the realm of thrown exceptions: A function in Swift that throws must be declared to throw, and when you call it you must write a try keyword in the calling expression.

                                      That style of exception is called “explicit exceptions”. Probably the most common language that follow that paradigm is Java.

                                      In Java there are in fact two kinds of exceptions: implicit exceptions (inherit from RuntimeException, do not need a try block or forwarding via the throws attribute) and explicit exceptions (the compile will stop with an error if these exceptions are not handled).

                                      In my Java years I’ve seen a lot of “exception fatigue”: library writers like to provide detailed (explicit) exceptions, but application programmers just make every method that uses those libraries “throw Exception” (equivalent to a catchall “ignore and forward”). (For the record: I do not think that that is a bad pattern in itself.)

                                      1. 2

                                        Sorry, I think you’ve missed my point. I’m not talking about try block statements as in Java. (In Swift that would be the do keyword).

                                        Here’s another attempt: Swift requires the try keyword on the specific expressions that can throw, just so the reader can tell which lines might throw out of the normal flow of execution. To my knowledge, Java has no equivalent.

                                        do {
                                            let encrypted = try encrypt("secret information!", withPassword: "12345")
                                            print(encrypted)
                                        } catch {
                                            print("Something went wrong!")
                                        }
                                        

                                        Here, encrypt may throw out of the do block. Print will not. You can tell when reading it! Code sample borrowed. It’s also worth noting that the try keyword does not do anything. It’s like a required comment.

                                        If at first you don’t explain try, try again.

                                        For what it’s worth, Swift has errors, which are the application-level thing you throw and catch as above, and also has lower level exceptions raised by the operating system or CPU, like invalid pointer reads or divide by zero. Errors are explicit and checked and common; exceptions are implicit and unchecked and rare, and probably just crash, but are guarded against generally by language safety features. For instance, we don’t have a pervasive null check situation because we have optionals, so the nil keyword is not the same thing as a zero sentinel value pointer. Maybe someday functions will express in the language which kinds of error they throw, but that’s not the case as of Swift 5. In total, this arrangement helps cut down on the kind of error handling fatigue you mentioned.

                                    2. 6

                                      Go’s treating of errors as first-class citizens is, by far, my favorite way of handling errors. Most of my other work is done in TypeScript, and it’s absolutely bonkers that we’re spending all this time typing functions and return types and the like, but when it comes to errors, we basically throw everything about type checking out the window. Obviously TypeScript can only handle so much given the choices in JavaScript, but it still feels like a huge missing opportunity for the language.

                                      It’s actually gotten to the point where I’m considering making all my functions return tuple types, with a nullable error component just like Go:

                                      type Err = Error | null;
                                      
                                      async function getUser(): Promise<[string, Err]> {
                                          try {
                                              const res = await fetch('/me');
                                              const data = await res.json();
                                              return [data, null];
                                          } catch (err) {
                                              return ["", err];
                                          }
                                      }
                                      
                                      (async () => {
                                          const [user, err] = await getUser();
                                          if (err !== null) {
                                              console.error(err);
                                              return;
                                          }
                                      
                                          console.log(user);
                                      })();
                                      
                                      1. 3

                                        Someone once said to me they thought java style checked exceptions would have been ok, if they were inferred via type inference.

                                        1. 7

                                          The problem with Java isn’t checked exceptions, it’s that exceptions are used for stuff that’s not exceptional, like not finding a result. If exceptions weren’t being used where they shouldn’t, it’d annoy people far less than it does.

                                          1. 1

                                            I’m not aware of anything that Java-style checked exceptions can do that you can’t do with result types in a language with ML-style types.

                                            1. 2

                                              The only thing I can think of is that Java kinda let’s you combine unrelated exception types, while at least in Rust you would have to define an enum for it.

                                              Plus, Rust is kinda bad with enums, because the individual enum members are not real types on their own, so you can’t say “only this specific error case of this enum can happen”.

                                              But on the other hand, Java’s throws clause is basically a union, with exception supertypes mentioned in the throws clause subsuming subtypes, so they can accidentality be thrown away while coding.

                                              1. 3

                                                The only thing I can think of is that Java kinda let’s you combine unrelated exception types, while at least in Rust you would have to define an enum for it.

                                                In OCaml you can combine result types with polymorphic variants which work like a set of constructors and are combined and type-inferred by the compiler. Here’s an explanation and so far it seems to work pretty well. Basically, composable checked exceptions in ML.

                                                1. 3

                                                  Really cool, thanks for letting me know this!

                                          2. 3

                                            I totally feel your frustration with errors, but I think the tuple approach puts too much faith in the developer not making a mistake. Do you know if Go prevents returning both an error and value?

                                            I would probably go with something like this:

                                            type Result<T> = ({success: true} & T) | {success: false, err: Error}
                                            
                                            async function getUser(): Promise<Result<{user: string}>> {
                                                try {
                                                    const res = await fetch('/me');
                                                    const user = await res.json() as string;
                                                    return {success: true, user};
                                                } catch (err) {
                                                    return {success: false, err}
                                                }
                                            }
                                            
                                            (async () => {
                                                const result = await getUser();
                                            
                                                // You can only access result.success here.
                                            
                                                if (!result.success) {
                                                    // Since success === false in this block, err can be accessed
                                                    console.error(result.err);
                                                    return;
                                                }
                                            
                                            
                                                // Because we returned when success === false, TypeScript knows that this
                                                // has {success: true; user: string}.
                                            
                                                console.log(result.user);
                                            })();
                                            
                                            
                                            1. 4

                                              Do you know if Go prevents returning both an error and value?

                                              Of course not! It’s not a bug, it’s a feature.

                                              1. 1

                                                Do you know if Go prevents returning both an error and value?

                                                No, and that’s not really a bad thing IMHO. For example I wrote an email address parsing library which can return an error but also returns the list of email addresses it was able to parse, which is useful in various situations (e.g. “sent email to X, but wasn’t able to send to Y”).

                                                In general, you probably shouldn’t return a value and an error in most cases though.

                                                1. 7

                                                  That sounds like a problem waiting to happen. Like a C function that processes half your data and admittedly returns -1 but if you forget to check you might just go ahead and use the half-processed data thinking you’re done (e.g. who will ever notice that only 999 addresses were processed, not 1000 until 3 months later you’re wondering why there is a large discrepancy in what you excepted and what you got at which point it will be too late).

                                                  1. 1

                                                    Well the solution to that is to not forget to check it 🙃 But yeah, I get your point. Perhaps a better way would be to return an error with the mail addresses on the error struct, so you need to do something with the error, and if you’re sure you want to get the email addresses anyway you can still get them from the error 🤔 This way it’s at least explicit.

                                                    1. 2

                                                      Alternatively you can make that particular failure case (a partial success) not an error and return some kind of state struct where for each address it would state whether it was able to send it or not individually. Since there was some kind of success presumably. This is a discussion of API design, I am not particularly familiar with how that’s preferred to be done in Go, I just try to design APIs that prevent the user from shooting themselves in the foot.

                                                      1. 1

                                                        I just try to design APIs that prevent the user from shooting themselves in the foot.

                                                        Yeah, that’s a good attitude. In this particular case you really needed to ignore errors on multiple levels for things to really go wrong. I don’t remember the exact details off-hand as I wrote this over 4 years ago (and has been running pretty much bug-free in production ever since processing millions of emails daily, which is actually not a bad accomplishment considering there were many issues before) but I would perhaps do it different if I were to write it today, I was fairly new to Go at the time 😅

                                                        Generally speaking though the mantra of “check your errors!” in Go is strong enough that you can somewhat rely on it; you can run in to problems if you ignore errors and return empty data too.

                                                    2. 1

                                                      There are linters that tell you if you forgot to check.

                                                2. 1

                                                  Not sure if you like it but you can use the io-ts-promise library to chain async operations together like this:

                                                  fetch('http://example.com/api/not-a-person')
                                                    .then(response => response.json())
                                                    .then(tPromise.decode(Person))
                                                    .then(typeSafeData =>
                                                      console.log(`${typeSafeData.name} is ${typeSafeData.age} years old`),
                                                    )
                                                    .catch(error => {
                                                      if (tPromise.isDecodeError(error)) {
                                                        console.error('Request failed due to invalid data.');
                                                      } else {
                                                        console.error('Request failed due to network issues.');
                                                      }
                                                    });
                                                  
                                                3. 6

                                                  I don’t think Go’s error handling is awesome necessarily, but rather a good trade-off based on my personal preferences and interests. I think this is subtle yet important distinction.

                                                  1. 2

                                                    The idiomatic style of writing Go results in indenting of the error handling, while the happy path remains un-indented. I’ve come to really like this about Go and something I struggle with doing in Swift.

                                                    1. 2

                                                      That’s what the guard statement is for.

                                                    2. 2

                                                      One thing I wanted to call out - you no longer need to use an external package (github.com/pkg/errors) to get error wrapping.

                                                      If you want to read more about this they discussed it here: https://blog.golang.org/go1.13-errors

                                                      Was happy to see the stdlib introduce error wrapping. I continue to be impressed with the slow evolution of the language and introduction of features like this.

                                                      1. 0

                                                        What I found particularly grand about Error handling in Go is that it makes it really easy and obvious how to consolidate all of your side effects (logging) and statefulness at top level while having lower layers be relatively dumb. It lends itself very well to that “Imperative Shell, Function Core” paradigm.