> For more on this, Ding Yuan et al. have a great paper and talk: Simple Testing Can Prevent Most Critical Failures: An Analysis of Production Failures in Distributed Data-Intensive Systems. The paper is basically what it says on the tin. The authors define a critical failure as something that can take down a whole cluster or cause data corruption, and then look at a couple hundred bugs in Cassandra, HBase, HDFS, MapReduce, and Redis, to find 48 critical failures. They then look at the causes of those failures and find that most bugs were due to bad error handling. 92% of those failures are actually from errors that are handled incorrectly.
People who believe Golang is explicit and hard to ignore errors but think that Rust is implicit and easy to ignore errors have never used both languages, full stop.
I challenge you to provide me with an example of Rust code that ignores an error without explicitly and obviously doing so.
If you're writing alone in a vacuum without linters, then I agree that Rust is more explicit.
If you need sufficiently advanced linters to catch every case, your error handling is not explicit. Especially if those linters are not currently sufficiently advanced.
You're conflating "explicit" and "statically verified".
Agreed that static verification by default is ideal. Rust wins here.
I was thinking about the case where there is a `?` in the code but the reader glossed over it, thinking it didn't return an error.
> Unlike Go, you cannot accidentally ignore an error condition in Rust.
There is no way to discard the error without explicitly doing so, and even then you still have to decide on a non-error return value to return (because Rust functions that fail provide an error or a value and not both, unlike fallible golang functions). I agree, I think Rust is strictly better in this capacity.
> People who believe Golang is explicit and hard to ignore errors but think that Rust is implicit and easy to ignore errors have never used both languages, full stop. I challenge you to provide me with an example of Rust code that ignores an error without explicitly and obviously doing so.
You seem to be unduly defensive. This was never my claim. I have used and enjoy Rust. We're all friends here :)
The error is explicitly in the function prototype. Every code path that returns must ensure the result is an `Ok(T)` or an `Err(E)`. You just can't accidentally overlook this. Even if you do gloss over it when skimming, the error is handled and dealt with.
> You seem to be unduly defensive. This was never my claim. I have used and enjoy Rust. We're all friends here :)
I'm just tired of hearing that Golang's error syntax is explicit but Rust's is somehow implicit because it's fewer characters, even if they desugar to virtually exactly the same thing. Both are explicit. Golang's is verbose and (ironically) error-prone.
Some of us are mere mortals. We tire and make mistakes. Perhaps even unworthy of the mantle of "Rust programmer".
> I'm just tired of hearing that Golang's error syntax is explicit but Rust's is somehow implicit because it's fewer characters, even if they desugar to virtually exactly the same thing. Both are explicit. Golang's is verbose and (ironically) error-prone.
I already admitted sympathy to the viewpoint that Rust's error handling is technically explicit, and that Go's error handling would be improved by static verification that errors are handled. What more do you want from me?
if err != nil {
return err
}
i’m hoping they find a way to simplify thisThough personally I feel in this case defaulting to rethrowing uncaught errors is better. Since that's the 99% case. I'd rather it be zero line.
> Though personally I feel in this case defaulting to rethrowing uncaught errors is better. Since that's the 99% case. I'd rather it be zero line.
This is ambiguous with the "doesn't error at all" case. If you're looking at source code `foo()` you can't tell whether that's equivalent to `if err := foo(); err != nil { return err }` or just `foo()`. You have to check the function signature to see what the return arguments are (or in Java's case, whether or not it throws).
Unchecked exceptions are not explicit.
Most IDEs will conveniently put a red squiggle line beneath the exact call site as well, and show you the compile error when you hover over it.
And if you choose to rethrow it, you will need to add to your method an explicit annotation.
This is explicit and verbose. Explicit does not need to be verbose.
How much additional context needs to be there and how often is this convention used by simply duplicating the message with basically repetitive text?
panic(err) become "Problem with date formatting: invalid date format" or similar
I also think "antipattern" gets thrown around too often. This sounds more like a preference or convention.
I too would label it a preference.
To get out of the error handling tedium in our platform I largely opt to panic instead whenever viable, which gives a nice trace for free. (I am but human, and the error handling particularly grates once you have gotten used to just typing `?`.)
I think V2 errors are taking more of a lead from the pkg/errors error.Wrap approach anyway.
Programs which use `panic` as ersatz error mechanisms are fundamentally broken. `panic` expresses an invariant violation that's much different than normal errors.
For many errors, in many situations, terminating the process is quite reasonable.
In my particular situation, the greater system will restart failed processes, and retry failed tasks. I find this useful as in many cases my program can just die when something weird happens, simplifying it's own logic.
Correct. This is something I have to design for in the system anyway, because in practice anything can (and does!) die at unpredictable times. It's typically an inevitable fact of life that a machine/kernel/program will occasionally die, and your system has to survive that.
`panic` is not for business logic.
Neither are 500 responses. A perfect HTTP server never responds 500 and there aren't any situations I've ever encountered where there's any valuable error recovery or business logic left to be done once the server has run into a 500-worthy issue.
IIRC, the HTTP server in the Go standard library recovers panics and issues 500 responses, which is what I would expect it to do.
Panic isn't an ersatz error mechanism.
panic brings important context, but you're right in that it's not an annotation in itself. It's a program flow mechanism, but I'd argue it's very often utilized as an error flow one.
My larger point was that this still doesn't feel like an "antipattern" and I see that word thrown around enough as a conversation stopper that I've become pretty cynical about it.
Go proverbs: "Gofmt's style is no one's favorite, yet gofmt is everyone's favorite."
Plus once your code throws one error, every bit of calling code also needs to handle that error. The problem cascades through a codebase quickly. Seemed like a huge violation of DRY principles.
Rob Pike (my understanding as one of the main guys who created the language) has actually addressed this, it's a good read:
https://go.dev/blog/errors-are-values
Tldr refactor your error handling to treat it like code. DRY and SOLID principles would apply and etc. Article makes example of handle your errors in one place rather than 20 by using no-ops on remaining operations after an error occurs.
I don't actually agree with the choice, as it takes one key library which throws errors at every call (like I'm dealing with now) for this to just become a huge pain to do. I had to completely change business logic to implement his suggestion, which isn't always viable (and I'm subsequently finding that out that that wasn't completely viable for us first hand now). Also a lot more boilerplatey type no value add type code needs to be written.
I much prefer unchecked exceptions for the most part, but at least I can understand WHY error handling is the way it is in Go.
Checked exceptions would have been fine if they composed decently with rest of the language...
No I hung up my Go boots in 2018, so may be a little out of touch.
Rust integrates a bit better with existing software. Inclusion of Rust in Linux being an example. Go culture tends to value pure Go projects a bit more than Rust's, where Rust wrappers around C libs are more accepted and common.
Go is generally easier from a social point of view, there's not much to learn as it's largely a repeat of what people are familiar with. So introducing it in a workplace is easier as the on ramp is minor.
Ubiquitous greenthreading in Go seems to make the integration piece a bit harder, but the onramp a bit easier.