An Open Letter to the Go Team About Try
ardanlabs.com
ardanlabs.com
I want to flag especially that there is not, nor has there ever been, a plan to "push this through".
People consistently say error handling is problematic for them in Go. Overall I think people spend more time writing error checks than they should, and at the same time there are still too many mis-handled errors. We would like to see if we can find a way to make it both less work and more correct. Maybe we can, maybe we can't. The process is slow and needs to backtrack out of formerly-promising dead ends, and that's OK.
There's a very large amount of conversation right now around try, and that's good. Like I said in the tweets, community-wide discussions like this one are open source at its best.
> That would enable library authors to build whatever error handling conveniences they wanted
What I want from a language - especially if it's, like Go, aimed at really big project - is for it to be opinionated and narrowed in options. That's Bill Kennedy's point as well, they're adding a language level feature so now there's one more way to do a very common thing. And when there's multiple ways to do something, they will all get used.
"But what about conventions and code reviews?", you might ask; that's cognitive overload that, if there was only one way, would never be a thing. I mean another feature of Go is that it enforces a certain standard (with tooling and formatters), so that as a development team you should never have to waste time and energy on discussing or arguing about formatting.
I'm not against changing how errors are handled now, as long as they're standardized and the One Way to do things.
Care to elaborate on both points, or direct me somewhere I can learn more about these opinions? Thanks!
This page shows the most common words found in a large corpus of open source Go code on GitHub:
https://anvaka.github.io/common-words/#?lang=go
The top four most common words are "err", "if", "return", "nil". These are each more common than "func" and "string" combined. It's hard to square Pike's comment with that evidence.
I don't know if the try proposal is a good one or not, but I think it's pretty clear that Go's original approach to error handling is quite verbose. Whether it should be that verbose is an interesting question, but personally I think making the user repeat that exact same boilerplate every time adds little value. If you want code that can fail to be visible, at least make the boilerplate shorter, which I guess the "try" proposal accomplishes.
Each of those words are extremely common in Go code, even outside of the 'if err != nil { return err }` construct.
It makes sense they'd be more common than 'func' and 'string' because functions usually contain at least one if statement and return statement, and err is probably the most common variable name, and nil is used to represent the zero value of many different types. The 'string' word is only really used in struct fields, function signatures, and some variable declarations, but usually the string type is inferred on assignment.
Rob Pike is, very likely, using a different set of programs to judge.
It seems a lot of things in golang were done just for the sake of being different, with nothing to back it up.
"Making different choices than you would have" != "out of touch with research" - especially when they know the research better than you do!
Appeal to authority fallacy. Furthermore, this was evident several times when discussions about things like generics came up. I think they even admitted it at some point.
When you said that, was also the appeal to authority fallacy.
Kotlin, Scala, Rust, D, C#, Java, and C++ are not language research. They're existing practice.
The golang authors threw away all that (and are still dismissive of things like sum types) because of, well nothing really to back it up. The same reason they have null pointers whereas all new languages moved away from them, and some like C# are being retrofitted to address them. I think the rationale they gave to having null pointers is that "they're part of the underlying machine" or some ridiculous reason like that, which further proves my point that they're out of touch with established programming research and practices.
Because of multiple decades at Bell Labs, which was the computer science research facility at the time. And because of multiple decades of experience using many languages to write a huge amount of real-world software. And because of specific problems that they found with using C++ in their specific environment.
Look, you don't think go was the right way to go. That's fine. You even base it on having used go and not liking it. It's good that you're basing your opposition on experience, not just theory. But don't claim that the go authors are ignorant of programming language theory just because of the direction they chose. They are not ignorant at all, and they chose the direction they did with full knowledge of what others were doing. It may turn out to be a mistake, but it wasn't from ignorance.
I'm genuinely interested to see the real-world software you're talking about. Other than UTF-8 (and that's mainly a spec), what's there to show? No, Plan9 is barely used. Most of their work was in C either way.
> And because of specific problems that they found with using C++ in their specific environment.
They found problems with C++, sure, so they made a language that addresses problems with C++ that other languages already did, without looking at what said languages did (e.g. Pascal - and even Java and C# - for fast compile times, and C# and ML-languages for generics). Again, they admitted that they were narrow sighted and mainly looked at C++, not what other languages that actually addressed issues with C++.
The golang authors are not language designers, that's always something to keep in mind whenever looking at what (mostly bad) decisions are being made in the language. The issue is, people are just looking at the brand behind the language and are not thinking objectively, leading to more unmaintainable code bases to deal with. Perhaps it's a good thing for software engineers though, who are going to be paid to clean up this mess :)
whether that's a good thing itself is another conversation
Take an afternoon to go through the Go FAQ [0] (they answer why they don't have exceptions), design documents [1], including this fairly recent one [2] about error handling - they explicitly discuss exceptions and the pros / cons - and that's just what ended up in the design documents / FAQ, there's been at least ten years worth of discussion gone into that beforehand. Do some googling before making accusations like that please.
[0]: https://golang.org/doc/faq#exceptions [1]: https://github.com/golang/go/wiki/DesignDocuments, https://github.com/golang/proposal [2]: https://go.googlesource.com/proposal/+/master/design/go2draf...
It's there in the first link you posted for example, "we believe exceptions lead to convoluted code". Belief here is not sufficient, it must be conclusively shown that it's otherwise, especially given that subjectively speaking it is possible in golang for errors to slip and not be handled at all (I saw it in real code bases), much much worse than any "convoluted" code that exceptions may result in (again it's very subjective - the way golang handles errors results in much longer code that's harder to follow).
Every editor has snippets, or the equivalent, making boilerplate simple to write. And you adapt to reading it pretty quickly - it just becomes punctuation after a while, and you only notice it if it does something unusual.
which is my problem with "try", and why I won't be using it if it gets accepted. It breaks the rhythm of "do something, check for errors, do something, check for errors" that Go code has, and that becomes easy to read.
Just yesterday I made the joke that you can recognise a Java developer at a party because they start almost every sentence with "public static void". Ha. See, funny?
Go's verbosity feels different, and I don't think it's "verbose". It's clear. It's about making it really clear what is happening.
What surprises me about the proposal is that they think that go's error handling does not reduce the number of errors, because in my experience it does in fact reduce the incidence of run-time errors significantly, and also makes it very clear how to maintain error handling code.
I'm not sure I like the try proposal, but I'm not sure I hate it. What I do know is that the verbosity you're complaining about doesn't feel to me like the same kind of verbosity I complain about in other languages, somehow.
Edit: grammer and spelling
---
I like writing “if err != nil { … }” so while I disagree with the proposal to add the error check function “try()” I will keep an open mind because I do not have the technical literacy as @griesemer or other people in the Go community to propose something better. I’ll experiment with “try” a few times in personal projects, but I can see myself writing “if err != nil” for several years unless I find a more compelling reason to change, same reason why my team and I haven’t adopted “go mod” despite having Go v1.12.6 (latest stable version as of today) in all our production servers.
[1] https://github.com/golang/proposal/blob/master/design/32437-...
[2] https://github.com/golang/go/issues/32437#issuecomment-49926...
[3] https://github.com/golang/go/issues/32437#issuecomment-50061...
[4] https://github.com/golang/go/issues/32437#issuecomment-50187...
That idiom was my biggest turn off in learning Go.
I would be 100x happier with "if not err {...}"
Personally I don't want error handling changed at all. It's very repetitive in it's current state, but clearer than in any other language I've used.
Controversially I do support generics in go2.
Most of blame for the mess is that the error interface throws away meaningful information. Then the caller is left mentally checking how to handle the error. Do I value check? Type cast? String check? Does the library expose a function to check, or do I do it myself? Can I get stack traces in some cases without introducing a production cost?
I'm not sure if Go needs a different type of error interface (ie diff methods), or if it needs Generics to solve this properly in my view. But, I just know I've hated the experience.
After I switched to Rust it all felt so familiar, and yet so different. I believe Rust and Go share error patterns. Yet in Rust I'm never left wondering what an error is, what I should do with it, how I check for specific types. This is largely due to awesome enum support and of course generics. Does Go need this feature set to "fix" my complaints with errors? Probably not.. but I do think it needs something.
Though I will agree, I don't think the `if err != nil` is fundamentally the problem with Go's errors, at all. Sure, the UX could be improved, but that's another story all together imo.
Edit: spelling, genetics to generics.
Without generics you would have to either throw away the type information, reimplement a `Result` type for every value type, or copy/paste the implementation at each usage site (the common Go pattern).
Go's support for multiple returns itself is also a hack around the lack of support for tuples (which would be trivial to implement if you had generics).
To a first approximation, the purpose of Go is to stop every codebase from reinventing its own tuples or Result<> type.
Go does have generics, the Go compiler could add a chaining syntax for trailing err interfaces (like rust ? / try!) without needing to allow user-defined generic types.
The ecosystem exerts a lot of pressure on developers to not reinvent fundamental things, in Haskell I practically never reach for a StrictTuple or StrictEither even when I'd like strictness specifically because they are clunkier and less well integrated with other libraries.
Scala is the only language that comes to mind with this issue (scalaz vs Cats) and my impression is that even they have mostly converged.
I would say Go has much stronger idioms than either of these languages, and wouldn't need to be afraid of people inventing their own fundamental types provided they're included in the standard library.
C/C++ is another example. There are a plethora of string types mostly tied to which base set of libraries you're using. If you're using the C++ stdlib, you have std::string. But if you're using Qt, you're using QByteArray and QString instead. And probably similar for GLib/GTK.
(Does std::string even have any encoding support yet? When I last used it, it was just a string of bytes without any encoding awareness.)
Rust has this too. You can throw away type information and incur a runtime cost just like with Rust by using "Trait Objects", which are Rusts version of interfaces, basically. Luckily most choose not to use this for error communication.
However I struggle to understand that throwing type information away and incurring runtime cost is the pinnacle of error communication.
Furthermore, the idea that this somehow prevents codebases from "reinventing" anything is absolute nonsense. Basically every Go lib out there has different error handling. Hell, I wrote a library of my own (similar to pkg/errors). The very items I listed in my OP showed how every lib has it. You have no idea how each Go lib reinvented the wheel to know how you handle their errors. All with runtime checking. Madness.
I think the important part there - and the contrast with e.g. exceptions - is that an error is treated as normal, as one possible outcome of calling a method, instead of exceptional as exception-based languages seem to be. I mean yeah of course a file can not be available to open, that can be expected. (That is the simple use case of course, I'm sure there's much more complicated situations).
I used golang at an employer and you're right, it's a huge mess (and error handling is just one part of it).
However a few decisions are huge signals that they didn't really think through them. Error handling and the now-solved package management are two big ones.
I keep seeing those claims (e.g. "programming in the large"), but in my experience, practically all the decisions in the language make it more awkward and difficult, even unnecessarily so, to deal with large code bases.
golang's interfaces for instance, are optimized for a small use case, at the expense of them being useless for actually useful things, like tagging and fast code search. The way "methods" are assigned to structs is overly verbose, and makes it easy to have to jump around even several files to figure out all functions that are tied to a struct. The list is too large to list here.
Compared to my Rust code bases, I feel like you need a larger picture of the project to understand what smaller code pieces do.
Is that a good thing though? Not sure, honestly. Should I really feel I understand what a function does, without understanding the functions role on the project itself? How can I truly understand a function without understanding its role?
Go feels like it's easier to understand the implementation, but not the context/purpose. Arguably, this may not be good.
I still prefer Rust, fwiw.
Again, what specifically about golang that makes it that way, other than errors vs. exceptions (keeping in mind that any line in golang can panic)? And what's preventing you from writing similar code in another language? I used Scala before and we had Option and Result types, and treated exceptions as panics. The same applies to Rust.
I found that because of golang's verbosity, it actually made it harder to understand what the entire project was doing. e.g. code bases I worked with were littered with one off functions that amounted to what basically was a map or map+filter, where this would have been a one liner in any modern language, including Java, C#, Kotlin, etc.
Basically, I'm agreeing with the rest of your comment. I find that a "denser" language (to a certain extent) allows you to understand the overall project quicker. There's probably a sweet spot somewhere, and I feel that languages like Java and C# are quite close to it. Keeping in mind that you want a high level overview of a project when you start off anyway, there's no magic solution that will give you that.
It's just that I keep seeing the same claims repeated so many times, and it really seems that they're just being repeated without any basis at all, and repeated just because what the golang author's claimed. So far, almost everything I've seen has been hype.
The proposal has been partially accepted and implemented in Go 1.13: https://github.com/golang/go/issues/29934#issuecomment-48968...
Also computing is all about eliminating or automating repeaditve tasks. having to write out (almost) the same code after almost every function call goes against the core usefulness of computing.
func foo() error {
a, err := bar()
if err != nil {
return err
}
b, err := baz(a)
if err != nil {
return err
}
...
}
In some code this error handeling is 3/4 of the function lines. Sure the error behavior is important but is it 3 times more important then the logic of what you are writing?The if nill construct contributes to cognitive load and detracts from code plainly tells you what it does. And speaking from a practical perspective I’m going to be far more productive if 3 lines of commonly used code is turned into five characters.
After a while of working in Go, you get used to the rhythm and you stop noticing the "check for errors" part unless it does something different. You also notice when the check is missing.
And having most of your code devoted to error handling makes total sense. There are usually lots of errors states and only one "golden path" state - properly handling all the possible errors your code could generate will mean writing more error-handling code than logic code because there are more error states than non-error states it can get into.
Instead of "do something, check for errors, do something, check for errors" we would have "do something, check for errors, try something, do something, check for errors" (assuming not all errors are going to be "try"d, and some will need to be wrapped).
It stops being obvious when I've missed an error-check. That's bad.
Either you have used try and therefor there has been a check for errors.
Or you have not used try and therefor you had to assign to err and therefor you have to use a check for errors.
Any assignment to err indicates you need to check for errors just like it is currently.
In go if something is truly exceptional then you can panic, but most of the time an error is non-fatal outside of a particular request (request not just related to a web request).
I agree, many people do errors incorrectly, but `try` does not make them suddenly correct either.
It's quite healthy for the community to have this discussion because this is introducing not just a new built-in function/expressiony thing, but also a means of flow control.
panic() is just another symptom of the same disease.
Exceptions are a very useful tool, and so is pattern matching and support for multiple returns. There is no one true way to handle errors.
No, an uncaught exception crashes the program. I guess you can generically catch all exceptions and get the same affect.
> panic() is just another symptom of the same disease.
Not sure what you mean here
> There is no one true way to handle errors.
Never said there was. I only made a comment that `try` does not fix how people are handling errors.
It's probably also biased due to the people who gave up on Go completely because they saw it as a sufficiently major issue and/or had little faith in Go ever improving (I can't exactly put a finger on it, but a lot of the decisions in Go seem to be backed by "... and if you don't like our approach, you're WRONG").
I don't understand this complain. Ultimately that's how most if not all programming languages are developed. A few individuals act as guide and gatekeepers and that's fine.
This was also my experience interacting with the Golang team while working at Google.
Python and Java got this right: everything throws an exception, including out-of-bounds access (IndexError, ArrayIndexOutOfBounds), so by wrapping your whole application in a try/except block you can eliminate a whole class of otherwise fatal errors.
Unless I missed something, the upcoming change isn't going to alter the handling of OOB's, which seems like a missed opportunity because a fully-fledged try/catch mechanism could effectively defuse many bugs.
It depends on whom you ask. Some people quite like checked exceptions. Either way, both checked and unchecked exceptions are superior to golang's approach to error handling.
stream.map(f).collect(…)
should obviously be able to throw anything f could throw. But instead f is forced to wrap every checked exception, and so pretty much everyone has given up and started declaring new exceptions as unchecked.The "try" proposal is about handling routine, expected errors.
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
})
http.ListenAndServe(":80", nil)
}
becomes FIRST package main
AND THEN
AND THEN import (
AND THEN "fmt"
AND THEN "net/http"
AND THEN )
AND THEN
AND THEN func main() {
AND THEN http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
AND THEN fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
AND THEN })
AND THEN
AND THEN http.ListenAndServe(":80", nil)
AND THEN }
there is a very good chance it would not make it into "the biggest challenge you personally face using Go today." Because it's not a challenge. It's stupid, but not a challenge.So just because the verbose incantation
if err != nil {
return err
}
or other issues, could be thought of as very similar to AND THEN, it doesn't mean that this is the biggest challenge someone personally faces. it could be stupid, but you get used to it.so it depends on the wording of the question. Basically the real question isn't how big of a pain point it is. It's how much it could be fixed.
my specification for the formatting of the language would be hugely improved by removing the AND THEN line from each line of the code. It's kind of ugly.
Picking on Java, it has checked exceptions, unchecked exceptions, and errors. It's basically a minefield of complexity leading to undefined behavior and/or misbehaving error legs. I can't tell you how many times I've seen InterruptedException mishandled, Throwable (which includes VirtualMachineError) caught with no appropriate handling (quick - what is the appropriate handling of a VirtualMachineError?), or exceptions allowed to wildly propagate.
Wowza, that's a strong stance.
Look, I'm a fan of Go and Java is definitely aged in a lot of places but this is one of the most bizarre claims I've seen. Criticism of Java's exceptions come from both sides, some people who hate unchecked exceptions so "anything can throw" (same reason C++ camp often avoids exceptions); or that checked exceptions are stupid since you can't meaningfully fix Interruptions or IOExceptions so it's verbose and meaningless to force local catches; they almost always need a rethrow. Both of these criticism are fair because in the end they both make sense sometimes and thus are misused for one another occasionally. We'd all like one answer consistently applied but we don't have it here.
Error codes formalized as a part of an API have their place but I find their utility is much more niche than people give them credit for. Why should I decode error codes when a String and a Trace will do? Hint: only when errors have formal semantics such as within a protocol. How I handle the error is partially decided by spec.
But to say that in comparison to Go? Go has its normal err mechanism and panic/recover. err is a huge step up from C style errno/manual checking. In practice, Go's err is embarrassingly used just like a checked exceptions: observe and ignore/log or propagate. You end up just unwinding the stack manually, losing the trace, maybe adding context occasionally, like 70% of the time. You handle it where it's meant to eventually, sure.
Like everybody does, when it is done right.
panic/recover is just unchecked exceptions all over again anyway. They seem to be treated as voodoo idiomatically, sure, but they're there.
Go is a great language that brought some really good language runtime features together in a pretty tidy package. Besides maybe defer, which is pretty clean, error handling just isn't a part of the language that got the polish that other parts of the language got on the first pass.
Error handling in languages with exceptions is (1) more robust, as you actually have to make an effort and explicitly ignore exceptions, and not just accidentally forget to handle them or overwrite them as with golang. (2) you don't have to incur a branch for every error check, which should result in faster code in the non-exceptional case. (3) the code reads easier and clearer, especially where you have to chain calls, which in golang will take up much more code. (4) Exceptions are concrete types, and give you a stack trace automatically, unlike golang where errors are mostly strings and if you need any context you have to manually populate it.
I’d much rather see some kind of genetics support get well integrated, given that’s a problem I run into fairly often.
It seems like a mistake to add a language feature that works against explicit handling. At minimum, the endorsement of returning the raw error reduces clarity on what "good error handling code" should look like.
I'd prefer to see an official errors.Wrap function instead of "try", for the reasons you cite.
(shout out to Dave Cheney's excellent "pkg/errors" implementation of error wrapping)
> If you look closely, error handling is not even in the top 3 challenges faced by developers, it’s number 5
Nonono, that's not what those numbers mean. 5% said it was their absolute biggest problem. For all you know it could be the #2 problem of 90%.
info := try(try(os.Open(file)).Stat())
I'm worried about nested versions of this and having to unpack them when reading code, which at the current moment has a much easier imperative block structure.[0] https://github.com/golang/proposal/blob/master/design/32437-...
f := try(os.Open(file))
info := try(f.Stat())
This can be further improved by applying special syntax highlighting that makes the try less prominent.Meanwhile, the cleanest way to write it right now seems to be:
f, err := os.Open(file)
if err == nil {
return err
}
info, err := f.Stat()
if err == nil {
return err
}
That's 8 lines of code to do the same thing. Did you spot the bug I introduced, or did you miss it because the volume of code made you skim? (error check has == instead of !=)?If you want to judge code by the worst thing you can do, you'd need to compare with things like:
var info os.FileInfo
if f, err := os.Open(file); err != nil {
info, err = f.Stat()
}
if err != nil {
return err
}
(Did you spot the bug? If err is already declared somewhere further up, this will compile <https://play.golang.org/p/0l-SsEbKQh2>, but the "err" within the if shadows the previously declared one, so the errors from os.Open and f.Stat are never checked.)And you return the error as-is if its nil; seems fine to me?
Your second example is also a bit... far-fetched because it tries to be smart by putting the error checking in the same line. There's also the "line of sight" guideline which states that the happy path should be left-aligned, while edge cases - like errors - should be in indentation [0]. See also the law of least astonishment [1], which your final example does not conform to.
Yes it will compile and yes it is an issue but that's not because of the language. It's because of an attempt at cleverness by writing more compact, of having the happy path in an if, and (arguably) by reusing variable names - but this seems pretty common, and longer variable names like idk, `osOpenError` and `fileStatError` seem to be discouraged in favor of reuse.
So I'm not sure if you're getting your point across like this.
[0] https://medium.com/@matryer/line-of-sight-in-code-186dd7cdea... [1] https://en.wikipedia.org/wiki/Principle_of_least_astonishmen...
Far better to use something more explicit and controllable:
f, err := os.Open(filename) onerr return 0, err
which is just syntactic sugar for: f, err := os.Open(filename)
if err != nil {
return 0, err
}
also: err := DoSomething() onerr {
fmt.PrintF("We got error %v\n", err)
return 0, err
}It's clear that there's way too much boilerplate in go procedures that deal with error handling by systematically bailing and passing the failure upstack...
But the most confusing thing to me about try() is that, although it's shaped like a function, it actually returns from the current scope on error. It's dangerous syntactic sugar IMO to make this one function-call-like feature not behave like an actual function call...
So it's legal to write something like...
panic(panic())
or a := try(try(f()))
...?Go's greatness could very easily be destroyed by a small number of missteps like this one.
I'm wishing for the absolute rule of the curmudgeony quadrumvirate like the one that designed Go. This feature-bike-shedding hungry mob is proof of the problem with democracies.
And when it comes to programming languages (unlike societies) the the trade-offs for a democracy are not worth the price. Go should (almost) completely ignore the crowd on design issues. It should have changes made that a small cabal of experts unanimously agree with.
Maybe a stack trace would be nice in the errors, though
The article suggests that try() isn’t solving a big problem. If I were a Go programmer, I think I would want a bigger change, specifically, a proper sum type for errors. In Go, the convention is to return a result and an error despite the fact that essentially every function that can return an error wants to return a result or an error. This results in complexities in the proposal about what the result should be when try() causes an error return. The sensible answer is that there should not be a result.
The way that Rust (and Haskell and many other languages) do so much better than Go has little to do with fancy ? operators. It’s that the return type makes sense.
Of course, sum types without genetics would be odd, but I don’t see how this would stop Go — a special case specifically for “such-and-such type or error” seems consistent with the Go typing philosophy.
if err != Nil {
return err;
}
after every other line sometimes. It's verbose, manual, and doing the helpful thing like adding context for the error is repetitive and often omitted. As I've mentioned elsewhere, you essentially want an exception system.Some care still needs to be taken with how other language features interact with it. You'll probably want a sum-type-y way to catch exceptions as an expression eventually. What happens if exceptions are thrown from functions in defers, etc. But unlike more FP languages where error propagation is much easier in a soup of composed functions, Go is much more Java/C#-like. Most functions have much more important names and meaning which make stacktraces much more valuable.