7 ms·
Have spent a while in the py/js world but have switched to rust/go for part of this year -- biggest change is boilerplate relating to error handling. Automatic
by awinter-py 7y ago
Have spent a while in the py/js world but have switched to rust/go for part of this year -- biggest change is boilerplate relating to error handling.
Automatic stack capture for exceptions is something my language could conceivably do on my behalf.
Writing even 3 lines of code per function to propogate up the error is a huge pain, especially because it pollutes the return type -- MustWhatever() in go is much easier to use than Whatever() returning (type, err)
- sagichmal 7y agoYou're fighting the language. Unless you're writing prototypes where crashing doesn't matter, you should be writing error handling code first, and your business logic second -- so-called "sad path first" programming, c.f. "happy-path first" programming that you usually do in languages with exceptions like Python.
- zbentley 7y agoI'm super into the error handling philosophies of Go and Rust, but I don't think either of them is "sad path first". That implies that you can think about your error cases in a meaningful way before you have concrete business logic, which seems unlikely.
- sagichmal 7y agoOf course you can. Sad path first means, after you write your data types and interface signatures, writing the first stub implementations as func (t *Thing) Process(id int) (string, error) { return "", fmt.Errorf("not implemented") } and then filling them in gradually like func (t *Thing) Process(id int) (string, error) { dat, err := t.store.Read(id) if err != nil { return "", fmt.Errorf("error reading ID: %w", err) } cert, err := dat.ExtractCertificate() if err != nil { return "", fmt.Errorf("error extracting certificate: %w", err) } return cert.Name(), nil } and explicitly not func (t *Thing) Process(id int) (string, error) { dat, _ := t.store.Read(id) // TODO: error handling cert, _ := dat.ExtractCertificate() // TODO: error handling return cert.Name(), nil } Writing code this way, explicit error handling upfront, is fundamental to reliability (for a large class of applications).
- tsimionescu 7y agoSure, but in a better language, the second version already does the exact same thing as the first one. Your `return fmt.Errorf` is not exception handling, it is simply manual exception bubbling. It is boilerplate that you can forget to add, and that makes it harder to understand what the code is supposed to do. Maybe for the first error you are adding some context that the Read function didn't have, but for the second, the error handling code is doing absolutely nothing that an automatic exception stack trace didn't do. Even worse, with this style of programming, someone up the stack who would actually want to handle these errors has no mechanism to do, since you're returning the same type from both error cases. If they wanted to handle the certificate error but not the read error, they would have to parse the error message string, which is brittle. But if you did want to add appropriate context, your function would bloat even more. Not to mention that the standard library doesn't really help, since it generally doesn't define any specific error types to set up some patterns for this. Even in your example, from the start you assumed that your function can either succeed or fail in a generic way, you didn't think that the signature may want to encode multiple different error types, which is what GP was talking about when saying you can't usually think about the sad case before the happy case. Sure, if the extent to which you want to define sad case first is 'sad case can happen', you can. Go's error handling strategy is its weakest aspect, and it is annoying to hear advocates pretend that Go is doing it right, when 90% of Go code is doing the same thing as 90% of exception-based code, just manually and with less helpful context for either logging or for the 10% of code which actually wants to handle errors.
- alessandrasa 7y ago`If they wanted to handle the certificate error but not the read error, they would have to parse the error message string, which is brittle.` Aaargh that is my main pain with Go error handling. I try to have an open heart with all the verbose way of living, but this "select by error type" when I want to treat errors really feels awkward
- sagichmal 7y ago> Sure, but in a better language, the second version already does the exact same thing as the first one. Your `return fmt.Errorf` is not exception handling, it is simply manual exception bubbling. You look at what I'm doing as a more tedious and error-prone version of exception bubbling, but that misses the forest for the trees. The whole point of doing it this way is to lift errors out of the shadows of the exception control flow path, and put them front-and-center in the actual logic of the application. Programming (in many domains) is error handling, the error handling is at least and arguably more important than the business logic. I don't want exceptions. I do want this (or something like it). > Even worse, with this style of programming, someone up the stack who would actually want to handle these errors has no mechanism to do, since you're returning the same type from both error cases. As the author of this module, I get to decide what my callers are able to see. What I've written is (IMO) the most straightforward and best general-purpose example, where callers can still test for the wrapped errors if they need to. If it were important for callers to distinguish between Read and Certificate errors, I would use sentinel errors e.g. var ErrCert (if the details weren't important) or custom error types e.g. type CertificateError struct (if they were). Adding this stuff isn't bloat. Again, it's just as important as the business code itself. > Go's error handling strategy is its weakest aspect, and it is annoying to hear advocates pretend that Go is doing it right In industry, considering languages an organization can conceivably hire for, and considering the general level of industry programmers -- programs written in Go are consistently in the top tier for reliability, i.e. fewest bugs in logic, and fewest crashes. Certainly more reliable than languages with similar productivity like Python, Ruby, Node, etc. There are plenty of flaws in Go's approach to error handling -- I would love to have Option or Result types, for example -- but I think, judging by outcomes, it's pretty clear that Go is definitely doing something right.
- andrepd 7y agoRust has a postfix `?` operator to propagate errors up the call stack.
- awinter-py 7y agoas I understand it, this requires an Option or Result return type that matches the type of the statement The ? operator is better than nothing, but I still need my error types to match all the way up the stack
- weberc2 7y agoIt doesn’t require it. Instead it could just generate the same error handling boilerplate. But I think it would be a mistake versus general purpose (generic) sum types a la Rust.
- dnpp123 7y agoAFAIK if you properly define the From/To operator, rust generates it for you. And if you think about it, defining those operator is a good practice as it tells your program how to handle errors.
- status_quo69 7y agoOr you can use a boxed error iirc. Matching on the type of error after you do that isn't exactly great but it's doable if all you want to do is bubble the error up. Places where that approach really sucks include web api routes (unless I haven't figured out a trick yet). For example, rocket defines Responder on Result so long as the ok and error variants both define it, but since a boxed error is erased it doesn't function correctly.
- onei 7y agoYou can use the Error trait [1] from the stdlib, implement it for your error types and then just use the trait in your Result. That way you don't need the types to match and you can convert between then as needed while adding the previous errors to the chain. 1. https://doc.rust-lang.org/std/error/trait.Error.html https://doc.rust-lang.org/std/error/trait.Error.html
- weberc2 7y agoI don’t buy the “huge pain” argument. I write lots of Python and Go, and the error boilerplate is a non-issue. I also appreciate that it’s explicit instead of implicit.
- malisper 7y agoIt's not just a pain to write. I've accidentally introduced way more bugs through Go style error handling than through Python style error handling. Some examples: Forgetting that a function returns an error: ... foo() // foo returns an error that isn't being handled. ... Forgetting to check the error returned by a function. Note a linter won't pick this up since the err variable is used later. ... err := foo() err = bar() // The previous error will go unhandled. ... Accidentally typing return nil instead of return err: ... if err != nil { return nil } ... And in the case of the errors library, there's times where I will call a builtin function that returns an error and forget to call errors.WithStack. Every once in a while I'll come across an error without a stack trace and I'll have to hunt down where it came from: ... err := json.Unmarshal(bytes, &obj) if err != nil { return err // should be errors.WithStack(err) } ... All of these issues look just like normal bug free Go code. On the basis that I've introduced more bugs this way, I prefer Python style error handling by far.
- sagichmal 7y ago> All of these issues look just like normal bug free Go code. Not to me.
- alphachloride 7y agook
- malisper 7y agoI don't know. They're hard for me to find. How would you pick up on the first and last example? They are possibly correct depending on the exact functions you are calling. The other two examples, I could maybe understand, but they still look pretty close to normal Go code. In the case where you forget to handle an error, you need to be able to recognize the absence of the error checking bit. It would be one thing if there was extra code that looked wrong, but looking for the absence of code makes it hard to spot. In the case where you return nil, it looks exactly like a normal early-exit from a function. You need to be able to recognize that the code is not a normal early-exit, and that the three letters "err" were swapped for the three letters "nil".
- EdwardDiego 7y agoWhat surprises me as a primarily JVM developer now supporting Go apps and dabbling in Go development is that the logging libraries (like klog) I've encountered don't support the level of configuration I'm used to in JVM apps. I miss being able to configure individual loggers at desired levels with a logback.xml in the JAR, but especially the ability to change logging levels at runtime. I guess it's part of the Go philosophy - more dynamic logging would most likely incur relatively higher performance costs.
- alaties 7y agoYou might wanna look at using something like zap instead, as it enables you to do runtime loglevel changes...
- EdwardDiego 7y agoCheers for my development, I'll do that :)
- LessDmesg 7y agoGo is just a half-baked joke language. Neither generics nor proper error handling nor a proper type system - whoever uses that crap just because it was made by Google is wasting their time. The only thing Golang has been succesful at is proving Google is well-capable of releasing and marketing absolute unadulterated crap. Just use the JVM or .NET Core...