3 ms·
My preference is either a Result-object or the Go convention of (result, error). Both force you to handle the error or explicitly ignore it, which is the corre
by theshrike79 4y ago
My preference is either a Result-object or the Go convention of (result, error).
Both force you to handle the error or explicitly ignore it, which is the correct way. Either you take care of it there if you have the context to figure out if it's relevant or not or you explicitly bump it one step up for the caller to handle.
- usrnm 4y ago(result, error) is very bad. It forces you to return some result even if there was an error. I've seen real production problems caused by using this garbage value after an error has happened.
- jspdown 4y agoCould you give examples of problems you encountered with this approach? The only issue I can think of comes from ignoring the error. You get the same problem with Rust's Result btw. Static analysis helps in this regard, golangci-lint catches easily such mistakes. In my experience, with production tooling, I never encountered what you described. Though, from a pure aesthetic point of view I prefer Rust's Result.
- red_admiral 4y agoIf you return an `Either[Error, T]` (to use Scala syntax) then in case of error you never have to construct a T and the caller can never read one by accident on the error path, whereas if you return a pair (T, error) then you can hit at least three cases: 1. T is an int that's sometimes legitimately zero, if you return (0, err) and the caller isn't careful they can look at the 0 without realising it's not valid. 2. T is a 'pointer' type so you return (null, error) and the caller tries to dereference the pointer anyway (one example I've seen is inserting a logging statement for debugging purposes just _before_ the error-check, along the style of `logger.debug("foo {t.name}")` where the braces interpolate stuff). 3. T lives in a strongly typed world where there's no convenient null value available (maybe your language distiniguishes between "definitely a T" and "either a T or null") so you somehow have to construct an instance of a T even though you're never going to use it and it'll most likely be invalid in some sense. That doesn't mean the golang approach is bad - especially not in golang itself - but you do need to know how to use it correctly. Then again, using monadic error handling in golang would probably be strictly inferior - it's not that you can't define one, but the language doesn't have the syntactic sugar to handle it like e.g. Scala does, and I find that being able to use that sugar to get some readability for the approach is the whole point of monadic error handling.
- usrnm 4y agoCallee: creates a complex object and does several steps of initialisation. Last step fails, so it returns a partially initialized object and an error. Caller: checks the error, which is not fatal, but expects to get a nil in this case. Ends up with a non-nil partially initialized garbage. Yes, someone screwed up. Yes, it's a bug. Doesn't change the fact that there are better error handling approaches that eliminate this kind of bugs completely
- nordsieck 4y agoYou're right that the language doesn't protect you here. One easy way to prevent these sorts of errors is to always return default literals with errors (in the same spirit of "value == var" from c to prevent accidental assignment).
- haileys 4y agoFor starters it relies on implicit zero values in the language. These are dangerous from a modelling perspective: if you have a 0 integer, or an empty string, you can't really be sure if that's a meaningful value or merely a default. This is a bigger problem than just error handling. It makes it all too easy for invalid data to slip into your system. Implicit zero values are a bad design decision that cause all sorts of other pain. Consider the behavior of sends and receives on nil channels [1]. This is a fundamentally nonsense operation, but Go's designers are forced to define a behavior because nil channels can exist thanks to implicit zero values. [1]: https://dave.cheney.net/2014/03/19/channel-axioms https://dave.cheney.net/2014/03/19/channel-axioms
- tialaramex 4y ago> You get the same problem with Rust's Result btw How? Take the easy example, "clowns" is a string that's supposed to be an small integer. How do I get this wrong in Rust? We have to say what we want to happen when it won't parse which isn't ignoring it. let clowns: u16 = clowns.parse(); // Won't compile If we say we believe this won't happen we can express that, but when it does happen the program panics... let clowns: u16 = clowns.parse().unwrap(); // Panics if clowns doesn't parse as a u16 We can say what we actually want to happen, but how is that the same problem? let clowns: u16 = clowns.parse().unwrap_or(1629); // OK it's 1629 if it won't parse Finally we can write the C-style "Nothing will go wrong" attitude using unsafe, but I don't see that as a problem, if your first instinct is to reach for unsafe Rust you're a bad programmer let clowns: u16 = unsafe { clowns.parse.unwrap_unchecked() }; // Mark Baum says "Boom".
- skitter 4y agoSadly Go doesn't force you to handle the error (this passes go vet): a, err := strconv.Atoi("blah") b, err := strconv.Atoi("123") if err != nil { log.Fatal(err) } return a + b This could be fixed to emit a vet error, but ultimately it is inferior to only returning either error or success, instead of returning both even through logically only one exists.
- majewsky 4y agoNot to take away from your argument, but for what it's worth, there is a lint for this [1] which is available in golangci-lint [2]. If there is any Go developer reading this who does not know about golangci-lint, I wholeheartedly recommend looking into it. It's found a fair share of significant issues for me that otherwise would have required tests to uncover. [1] https://github.com/gordonklaus/ineffassign https://github.com/gordonklaus/ineffassign [2] https://golangci-lint.run/ https://golangci-lint.run/
- masklinn 4y ago> Both force you to handle the error That is definitely not true of the second, especially when the language is incorrectly or insufficiently pedantic like Go. Notably, because the base pattern is foo, err := Bar() it is very easy to write something like: foo, err := Bar() // code, no error check bar, err := Baz() // code, no error check qux, err := Qux() if err != nil { ... } And Go is perfectly happy with that, because while it will complain about unused variables it doesn't say anything about dead stores. You need external tools to warn about this issue, as well as ignoring the returns entirely. And then of course because this pattern requires having a valid "result" value which ends up in scope, it's very easy for bugs to creep in where this result value does get used even in case of error.
- nordsieck 4y agoIMO, that would look suspect to me. It's much more common to see if foo, err := Bar(); err != nil { It's a pretty immediate yellow flag for me if you're not doing that. In general, I've found that not catching errors is not a practical problem when writing go. Whereas things like interface nil ambiguity and the way defer and closures interact bit me a few times.
- mdaniel 4y agoThat's all fun and games if you want `foo` to be used in some `else` block but it just straightup wrong for using `foo` further down in the method as it scopes the decl (`:=`) to the `if` package main import ( "fmt" ) func Bar() (string, error) { return "hello", nil } func main() { if foo, err := Bar(); err != nil { panic(err) } fmt.Printf("foo is %q", foo) } $ go run foo.go ./foo.go:12:8: foo declared and not used ./foo.go:15:30: undefined: foo > In general, I've found that not catching errors is not a practical problem when writing go Yeah, good for you, but I am obviously not a consumer of your code because I have lost track of the number of times I have to use software written by folks that try to be heroes and program using dumb editors that cheerfully don't warn about swallowing err