4 ms·
My non-Go code is starting to look more like my Go code. One of the things Go taught me is that I was not being as careful about my errors as I should be. It c
by jerf 5y ago
My non-Go code is starting to look more like my Go code.
One of the things Go taught me is that I was not being as careful about my errors as I should be. It can be argued that exception-based handling provides you a nice baseline default, but it makes it way to easy when doing network or system-type programming to thoughtlessly default to that, when you need to be thoughtfully defaulting to that.
With sufficient care, exception-based programming and errors-as-values converge in the end anyhow in "code that treats errors correctly". But my exception-based code is a lot more informed by the errors-as-values approach now; a lot more try statements with catch statements that actually do something.
Even Haskell's very nice Either monadic handling can make it too easy to be in the heat of the moment and not thinking about what the errors actually mean and what I can do about them.
I don't think it's appropriate for every domain, which is why I qualified the code I tend to work on. But in those domains I'm thinking a lot more about how every single line of code can go wrong rather than leaning on default exception-based handling and expecting it all to work out.
- vlunkr 5y ago> One of the things Go taught me is that I was not being as careful about my errors as I should be. I identify with this so much. Especially when dealing with external things (file system, database, network) things can go wrong at nearly every step. And yeah, that means you have to check errors at every step, but it forces you to think about how you want to handle them, and what message you want to propagate when they happen. As a result, my Go code has very few unexpected errors in production.
- new_stranger 5y agoHard to overemphasize this. Handling errors is similar to the benefits that writing tests provides - slower upfront, but a more stable product gets shipped. Handling errors everywhere means problems are already solved before they happen. No 1 am pagerduty alerts because we didn't consider what would happen if a DNS server hung until we timed out and thought just wrapping in a try/catch and crashing was a good idea.
- closeparen 5y agoThings can go wrong at every step, which means each collection of fallible steps carries with it a combinatorial explosion of successes and failures. Do 5 fallible statements in sequence and it's 5+4+3+2+1 = 15 mock expectations you need to write. This tedious enumeration exercise is the majority of the time that goes into writing Go programs, for me at least.
- boloust 5y agoMost of the time, you'll return early after an error. To get into a situation where you'd need to handle n! cases, you'd need to keep running the statements after a failure, and then collate and return all the errors at the end. Not sure why you'd want to do that. You'd definitely need to go out of your way to do something like this, and probably start asking yourself why you're doing this pretty early on. Plus it would flat out refuse to compile in some cases where there are dependencies between the statements.
- closeparen 5y agoIt’s + not * in that example.
- boloust 5y agoAh, okay. In that case it becomes {n + 1 \choose n - 1} What code are you writing that results in this set of possibilities?
- jerf 5y agoI have some code where I use multierrors and collect everything into one big error (I highly recommend this for "validation"-type code, you should generally return all errors not merely the first), but what code are you writing where you're running 5 statements unconditionally but need to handle nearly-arbitrary combinations of them failing, and multierrors aren't what you need?
- closeparen 5y ago
- ohwellhere 5y agoThis is exactly my experience. Being forced to handle so many more errors showed me how much error handling I was inadvertently eliding in other languages!
- JoelMcCracken 5y agoI think this is for sure the first time I have ever heard someone say that Haskell makes it too easy to handle error cases.
- jerf 5y agoTo be more precise, it is the language that makes error-as-values so easy that it actually strays over into "as easy to forget about errors as exception-based handling". You can successfully build huge chains of code that are just blindly passing errors up without adding any context about where they happened or anything in a way that even Rust can't compete with. Haskell mitigates this issue by also having one of the more clever ways of dealing with errors, if you start getting into monad transformers or some of the advanced stuff, so it's not all bad. But it definitely can make it so easy to "handle" errors that you just forget about them.
- Karrot_Kream 5y agoI've used Scala in production codebases and it was common to just "let the EitherTs handle the exceptions". What ended up usually happening was swallowed exceptions that nobody bothered to handle and log because it was much less idiomatic Scala/FP to actually log what happened underneath than it was to return the first error that happened by short circuiting the EitherTs in for/yields (Maybe and do notation, for Haskellers). So we'd get 5xxes and log lines like "row not found" and have absolutely no idea why. I see a lot of the same issues in Rust code today FWIW. People just keep bubbling up errors and at the toplevel say "fuck it" and dump them, without _actually_ building a reasonable error chain to offer the programmer a story behind how the error happened. The fact is, error handling in any language is tedious. Whether you do it with a sum type, a product type, or Go's error values, either way there's going to be lots of verbosity and boilerplate.
- uncomputation 5y agoGo’s error handling is a strange this to praise considering it is entirely possible to ignore it without warning and the way to check error kinds was bolted on through type assertions when errors are supposed to be values in Go. Error kinds are important especially in network programming when the error can be dozens of different things. Compare this to something like Rust or Haskell which force you to handle errors and their variants.
- Thaxll 5y agoOnce again Rust does not force you to handle errors, you can ignore errors in Rust and it will compile. I can't say for Haskell but I doubt there is a language that forces you to handle error. let _ = openFile() Error ignored, it does compile, it will probably ends badly. As for network error: https://pkg.go.dev/net#OpError https://pkg.go.dev/net#OpError
- Karrot_Kream 5y agoPretty much every form of Go tooling warns you when you ignore an error. If you only choose to run "go build", yes you can ignore it, but a simple "go vet" call and it'll be plain as day.
- azth 5y agoa, err := foo() // oops, unhandled error err = bar() if err != nil { ... } or fmt.Println("...") // oops, unhandled error or more insidiously err1 := foo() if err1 != nil { ... } err2 := bar() if err2 != nil { return err1 } // oops
- Karrot_Kream 5y agoIn the first and the last case, it seems obvious to me where the error is. An obvious code smell caught in review. The second error, yeah, but lots of languages don't enforce error handling around printing to stdout, and even though Rust does in theory, in practice everyone just unwraps it and moves on with their day.