10 ms·
IMO Go's error handling is a flop. I constantly see code with the same verbose error checking that simply shoves the error further up the stack. No better than
by ilovecaching 8y ago
IMO Go's error handling is a flop. I constantly see code with the same verbose error checking that simply shoves the error further up the stack. No better than try/catch. Maybe and Either are much more elegant solutions that allow errors to be checked when values are needed rather than when they are created. I also really like monadic catch/throw.
Panic is an anti-pattern, I think, and seems oddly placed as I think the article was illustrating. Once again, these types of failures would be easier to represent with a richer type system.
- platinumrad 8y agoI also prefer the option monad, etc. over returning (result, result..., err) tuples but terseness of error handling is as much a property of the language (i.e. operators, features, etc.) as it is a property of the type system. Consider the "try!/?" pseudo-macro from Rust. If Go had macros, there would be nothing preventing something similar from being implemented.
- majewsky 8y agoIf Go had macros, it wouldn't have been as successful as it is. The appeal of Go is that it's both very readable and very writable, even if verbose.
- bbatha 8y agoGo already has code generation built into the standard tooling. It also has reflection which is arguably worse because the generated code is totally opaque. Good macros would cover most of the uses cases for the tools in a more cohesive and more tooling friendly way.
- cesarb 8y ago> Consider the "try!/?" pseudo-macro from Rust. On Rust, try! is not a pseudo-macro, it's an actual macro (which could be defined by anyone, since it uses only the public API for Result and From). Go to https://doc.rust-lang.org/std/macro.try.html https://doc.rust-lang.org/std/macro.try.html and click on the [src] link at the top to see its current definition.
- platinumrad 8y agoI called it a pseudo-macro because while "try!" is a macro, its usage is discouraged in favor of "?", which is an operator.
- londons_explore 8y agoYou mean a bit like NaN in floating point math? ie. code execution continues with the same flow, but any code that takes as input an error will return the same error.
- iainmerrick 8y agoI find NaN is mostly unhelpful on balance. It can help avoid crashes, but you get garbage results and it’s often hard to trace back to the underlying culprit. NaN means kicking the can down the road and failing as late as possible, but failing early means much easier debugging. Something like a NaN (for arbitrary operations, not just floats) that could also give you a full backtrace would be very interesting -- a bit like Java’s nested exceptions.
- munificent 8y ago> I constantly see code with the same verbose error checking that simply shoves the error further up the stack. I found this tragicomically illuminating: https://anvaka.github.io/common-words/#?lang=go https://anvaka.github.io/common-words/#?lang=go The four most common words in Go programs are literally: "if", "err", "return", "nil". If that doesn't tell you the designers missed the boat on something, I don't know what does. What I find frustrating is the attitude that went along with it. I think it's reasonable to say, "Yeah, error-handling is important but we think Java-style exceptions have too many problems. We don't want to do that, but we're not sure what a better alternative is yet. It's a hard problem." But instead, it's: "Our proposal instead ties the handling to a function - a dying function - and thereby, deliberately, makes it harder to use. ... If you're already worrying about discriminating different kinds of panics, you've lost sight of the ball." It's never the language being wrong, you're just using it wrong. Good error-handling is hard. Users want to be forced to handle errors that they want to ensure are handled, but not annoyed by errors they don't care about. Telling which from which is highly contextual, and the language rarely has that knowledge.
- dilap 8y agoI don't know. I find the distinction of "panic if it's a bug, return err if it's a normal condition" to work pretty well. It's much easier for me to write robust server software in Go than in either Java or Python -- because I don't constantly have to be thinking, "oh, is this thing going to suddenly unexpectedly throw?" Basically, exceptions for exceptional conditions is good. But Java and Python use exceptions for normal conditions, which is cumbersome and annoying. So for me, Go's approach to error handling is an improvement over just exceptions. The fact that it's verbose is fine if maybe not ideal. (My vague understanding (just from reading, not using) is that's rusts is similar but more ergonomic. Ditto for Zig.)
- atombender 8y agoThe problem in Go isn't that the semantics are wrong, it's that the error handling is so prevalent that it starts obscures your logic. I'm writing an app right now where almost every piece of high-level code is: if err := doThis(); err != nil { return err } if err := doThat(); err != nil { return err } if err := doThisOtherThing(); err != nil { return err } // ... The patterns in Go code are typically either (1) "call this and bail if error" or (2) "call this and bail if error, wrapping the error in a new error", with a sprinkling of (3) "if the error is io.EOF or whatever, do something else". The #1 case is so dominant that Go really cries out for a built-in error-propagation mechanism. The other problem with Go's error handling is the use of multi-value return types. I suspect Go's designers thought this was elegant, but it confuses things, because all error-returning functions return either a value or an error. Never both. It's not two return values in actuality, it's a sum type (aka product type or union). I'd take this over the current scheme: func GetFile() (io.Reader | error) { ... } And then maybe: switch t := GetFile().(type) { case io.Reader: ... case error: return err } which can have the following semantic sugar: r := try GetFile() Rust has a macro that does something like the above, but the type system supports sum types and matching already. Now you can do: r := try GetFile() count := try r.Read(buf) instead of: r, err := GetFile() if err != nil { return err } count, err := r.Read(buf) if err != nil { return err }
- cdoxsey 8y agoPractically the verbosity isn't a big deal. It's not hard to type and you can always copy and paste. More importantly, as a pattern it's pretty easy to read. The code and meaning is clear and you can follow it. Maybe some syntax would be nice to shorten it, but it really doesn't bother me all that much. You get used too it. Bigger problems are language warts which lead to bugs, and a nasty one has to do with interfaces and nil: https://golang.org/doc/faq#nil_error https://golang.org/doc/faq#nil_error . Seems like it would be much better for them to fix that. (and there are attempts to do so in issues on GitHub) FWIW the errors package is a nice way to further annotate errors: https://github.com/pkg/errors https://github.com/pkg/errors And at least with distributed systems error handling is a more complicated affair. The boilerplate ends up not being so boilerplate when some errors should be ignored, some retried and some should result in a panic. I guess there's an appeal to a well defined exception type hierarchy for something like that, though how that fits with a language like go isn't at all clear... in practice knowing the type hierarchy is also hard ... what's the difference between Error and Exception ends up being a common bug in python for example. We treat these problems like they have obvious solutions, but they don't. Fixing them the "easy" way ends up turning Go into a very different language, and it will be a sad day when that happens. Folks who enjoy working with Go will just fork a minimalist version, and the folks who complained won't use it anyway and will stick with rust or whatever. At least that's my sense of it.
- rmrfrmrf 8y agoIsn't Go's error handling basically just a procedural version of Either? It's a tuple instead of a sum type and the Left and Right cases are flipped, but otherwise I thought the entire point was that you're dealing with values that you only need to worry about at access time. Sure, it's not a proper functor, but for a procedurally-driven language design, I would think that the current design makes more sense for debugging.
- atombender 8y agoThere's a major downside to the multi-value returns compared to sum types: They can't be chained. You have to assign the two result values to two separate variables: s, err := readFile() There's no way to build something like this: readFile().then(func(s string) { ... }) Well, you can build it by having readFile() return some object with a then(), but without generics you can't build anything remotely ergonomic here. (Actually, Go has an interesting quirk where multi-value return values can be passed as arguments to a function, so this is allowed: func handleErrors(s string, err error) { ...} func readFile() (string, error) { ... } func main() { handleErrors(readFile()) } Unfortunately, there's no way to build a generic chaining mechanism with this, since you'd have to have a handleErrors() for every possible return type combination.)