7 ms·
There are several shortcomings with go's error handling. The author heavily lies onto rust, so the alternative is not exceptions but a `Result<T, Error>` sum ty
by the_gipsy 2y ago
There are several shortcomings with go's error handling. The author heavily lies onto rust, so the alternative is not exceptions but a `Result<T, Error>` sum type.
No stacktraces and error wrapping forces you to not only invent unique error messages. You must also conceive a unique wrapping message at every call-site so that you can grep the error message and approximate a stacktrace.
The weird "return tuple" , which obviously just exists for errors because there is not a single other place where you can use tuples in the language, and the awkward variable initialization rules, make it so that you use the wrong `err` var at some point. E.g. if you want to reassign the result to an existing var, suddenly you have to declare `var err error`, and if `err` already exists then you have to reuse it.
There should be an enum type in go, or instead of the bizarre "return tuple" mechanics exclusive for errors, they should have added a better syntax sugar for errors like rust's `?` sugar. Instead we have something extremely tedious and quite error prone.
> it has a lot of build in opinions which protect you from yourself
It does have opinions, but too often they seem to be there to protect the language from being criticized. Sadly, this works, as marketing (lying) is an important factor towards making a PL popular in today's market.
- KingOfCoders 2y agoExceptions are sum types, they just have different syntactic sugar.
- pema99 2y agoNot really. Exceptions usually imply unwinding the stack, and the ability to catch at any point throughout the callstack. Result types are just 'dead' data.
- zozbot234 2y agoThese are fully equivalent in outcome, though often not low-level implementation. You can use try...catch (called panic...recover in Go) to pack a normal and abnormal return case into the equivalent of a Result<> type. Or just pass an abnormal Result<> back to the caller to manually unwind a single "layer" of the call stack.
- biorach 2y ago> These are fully equivalent in outcome They are so different in DX, ergonomics, implementation and traceability that I'm not sure this is true other than in the most abstract sense
- sshine 2y agoThere is some DX similarity between checked exceptions and Result types. Because the compiler will fail if you don't explicitly mention each possible exception. But checked exceptions are coming out of style: They're unchecked in C#, and frameworks like Spring Boot in Java catch all checked exceptions and rethrow them as Spring Boot flavored unchecked ones. For unchecked exceptions and Result types: The DX is very different in one critical way: With Results you constantly have to differentiate between error and ok states, before you proceed. With unchecked exceptions you generally assume you're always in an ok state. It's equivalent to wrapping your whole function body in 'try { ... } catch (Exception e)'. And you can get that with Result types in Rust by using '?' and not worry about doing something half-way. Ultimately: Are you a happy-path programmer?
- Arnavion 2y ago>Because the compiler will fail if you don't explicitly mention each possible exception. But only the first time. Once you add `throws FooException` to the caller signature, the compiler won't complain about any future callees that also happen to throw FooException, even if you did care about handling their exceptions yourself. With callees that return Result you do get to make that decision for every callee.
- sshine 2y agoThat's a fair distinction. I would say "adding Result to the caller signature" provides a similar cascade, but it's not entirely true: Every call must separately be unwrapped. So consistently wrapping your Result calls with the '?' operator is similar to a gigantic try-catch block or adding 'throws CheckedException' everywhere. So both the '?' operator and adding 'throws CheckedException' everywhere let you accidentally neglect proper error handling: You have a default that is syntactically almost invisible and frees you from thinking. The checked exceptions give you a little more freedom from thinking than the '?' operator.
- veidelis 2y agoAnd different control flow, and different or sometimes non-existent types (Java's throws).
- deleted 2y ago[deleted]
- 9rx 2y agoChecked exceptions may be implemented as a sum type. Traditional exceptions are more likely to be a single type that wraps up a context object alongside stack trace information.
- masklinn 2y ago> The weird "return tuple" , which obviously just exists for errors because there is not a single other place where you can use tuples in the language MRV, go does not have tuples. Go is not the only language with MRV (as a special case) and they’re not necessarily bad, iirc Common Lisp uses them as auxiliary data channels and has a whole host of functions to manipulate and refit them. Go is singularly incompetent at MRVs though, in the sense that only syntax and builtin functions get access to variable arity (e.g. if you access a map entry you can either get one return which is a value, or two which are the value and whether the key is/was in the map). So MRVs mostly end up being worse tuples infecting everything (e.g. iterators needing Iter and Iter2 because you can’t just yield tuples to for loops).
- pwdisswordfishz 2y ago> MRV, go does not have tuples. > MRVs mostly end up being worse tuples I think you noticed yourself that you’re getting too hung up on terminology. Multiple return values are a half-hearted, non-reified version of tuples.
- masklinn 2y agoNo, MRVs can actually offer useful properties and features, that is what they do in Common Lisp. That Go does not do that has nothing to do with MRVs.
- erik_seaberg 2y agof(g(), h()) can't work with MRVs, only f(g()). multiple-value-list, multiple-value-call, and quietly ignoring unneeded values makes them much more usable.
- biorach 2y agoWhich is what they said. I'm not sure what point you're making
- robocat 2y ago> MRV Acronym for Multiple Return Values https://gobyexample.com/multiple-return-values https://gobyexample.com/multiple-return-values
- the_gipsy 2y agoI forgot: The tenet "accept interfaces, return structs" is violated all over by returning the `error` interface. IMO it's okay to make behaviour-exceptions specifically for error handling. Rust for example doesn't really have builtin behaviour exceptions specifically for errors, they're generic to sumtypes and just happen to work well for errors. But then in practice you must resort to thiserror or anyhow helper crates to deal with errors in anything but tiny programs. If you do make behaviour exceptions for error handling, be honest and upfront about it. Don't say "errors are just values so just use them like regular vars" in docs, if then there are several huge exceptions (tuple-returns and breaking a core tenet). If you make exceptions then you might as well do them right, instead of half-assing them. I believe zig does it right, but I haven't gotten around to try it.
- Seb-C 2y ago"Do what I say, not what I do" is almost a design guideline of Go at this point, there are so many inconsistencies like this in the language design.
- 9rx 2y ago> The tenet "accept interfaces, return structs" is violated all over by returning the `error` interface. To be fair, that expression came from a blogger who often wrote about Go. It is not a tenet held by the Go project. In fact, Rob Pike has made clear that he considers it to be misguided advice. It is only violated in the same way returning Result in Rust violates the assertion I made up for this comment: Do not return Result.
- the_gipsy 2y agohttps://go.dev/wiki/CodeReviewComments#interfaces https://go.dev/wiki/CodeReviewComments#interfaces
- wbl 2y agoHow on earth is that violated by error? error is implemented by types all over the standard library and beyond and consumed by functions that wrap errors in the errors package. It's exactly an example of what you claim is violated. Even more obviously your link isn't talking about functions but packages. There are some violations out there but generally when included packages define interfaces they are ones that get consumed like in io or it's dissolution.
- devjab 2y agoI prefer the structure of Rust errors as it’s fully typed, I don’t like that you can chain them though. It’s a cool feature but it leaves you with some of the same issues exception handling does when the freedom is used “wrong”.
- 9rx 2y ago> The weird "return tuple" , which obviously just exists for errors because there is not a single other place where you can use tuples in the language Go functions also accept a tuple on input. While theoretically you could pass an error, or a pointer to an error for assignment, it is a stretch to claim it is for errors.
- the_gipsy 2y agoYes exactly, that rather useless feature just makes the whole thing even weirder.
- deleted 2y ago[deleted]
- 9rx 2y agoA function that passes tuples, or multiple arguments as they are more commonly referred to as, is not particularly weird, and has pretty much been the norm since the addition of functions to programming languages. But you are right that languages whose functions only accept tuples, but not return tuples, is a strange curiosity. In practice, you end up with developers packing multiple, unrelated items into a single variable to work around the limitation, which then questions why not do the same for input?
- the_gipsy 2y agoI meant weird/useless in the sense of that it's used virtually nowhere in practice. There is a nice func Must(value, err) { if err { panic(err) } else { return value } that you can use in tests to get around the inane regular error handling. But that's about it, to my knowledge. Sad that there isn't more to it. If literally everything returns error as last return value, there could well be some syntax sugar there.
- 9rx 2y agoI see it used often, even in languages that don't formally support multiple return arguments – with some hacked up array/object single value return to try and emulate what would be better represented as multiple return arguments. Like I mentioned in another comment, Go does seem especially prone to attracting developers familiar with other languages who insist on trying to continue to program in those other languages even after working in a Go codebase. That may create a condition whereby the developers you regularly come across don't have a good grasp of where to use multiple return arguments effectively, and thus end up avoiding them, even where they would be appropriate. With respect to the syntax sugar, there have been a number of proposals for exactly that. While the proposed syntax itself has been well received, nobody has come up with a good solution for the rest of the problem. Syntax is only about 10% of what is needed, of course. Rust, for example, has well defined traits and other features to go along with its '?' operator to fill in the remaining 90%. Presumably there is a good solution out there for Go too, but until someone proposes it...
- packetlost 2y agoRust and Go's lack of stack traces are basically equivalent in that you need to call an additional function to add the stack context to the error result. For go you use fmt.Errorf, in Rust you use .context from anyhow (bad practice in many contexts IMO) or .inspect_err + log. It's rather unfortunate that neither has an easy way of capturing a line number + file easily and appending it to the context. Go could easily do it, I think. Oh well. I agree that Go should really have an analogue to Rust's `?`, but you can't really do that in a sane way without sum types to represent your conditions. The very multiple-return style error propagation makes it impractical to do. IMO Go should add really basic tagged unions and rework the stdlib to use them, but such a change would probably necessitate a v2.
- pdimitar 2y agoRE: Golang v2, they clearly said they will not do it and will double down on backwards compatibility with exceptions powered by env vars and/or CLI switches.
- packetlost 2y agoI'm fully aware of that.
- 9rx 2y agoTechnically, Go v2 signified the transition away from Google control to the project being directed by the community. That happened several years ago. Go v2 is already here and has been for a long time. The stdlib is also at v2 now (e.g. math/rand/v2). You must mean the language? They said that a language v2 (go2) is probably unnecessary – that any future additions could be added without breaking the existing language. I don't expect simple tagged unions would need to break anything. A v2 (or even v3, perhaps) stdlib would be necessary to take advantage, like the parent suggests, but that has never been ruled out and is already the status quo.
- packetlost 2y agoThis was what I was referring to. The stdlib is what would need to see backwards-compatibility-breaking changes, not the language itself.
- yegle 2y agohttps://github.com/pkg/errors https://github.com/pkg/errors provides stack traces support. This unfortunately did not get included in the Go1.13 release when error wrapping was introduced.
- dfawcus 2y agoAlef actually has/had real tuples, as does Limbo. Looking at Alef, apart from using tuples to provide multiple return values in a C-like language, they don't seem to actually add much functionality over what Go has without them. One of the few extra things is the ability to pass a tuple over a channel, however passing a struct is more or less equivalent. I've not looked in much detail at what Limbo could do with its tuples. So maybe they don't really add that much, hence why they were not carried over when Go was created? Alef had enums, Limbo has something like the Go constant scheme and iota. Limbo also had tagged unions / varient-records, in a sort of Pascal like fashion - the "pick adt".