4 ms·
Why I Prefer Exceptions to Error Values
- quantified 2y agoGolang has exceptions. They're just called "panic".
- pipeline_peak 2y agoThis blog post seems pretty biased. There are opposing rxamples online where not using exceptions is faster and even lighter https://gunnarpeipman.com/cost-of-exceptions/amp/ https://gunnarpeipman.com/cost-of-exceptions/amp/
- d35007 2y agoThe article's title is "Why I Prefer Exceptions to Error Values". It literally tells you that it's biased.
- pipeline_peak 2y agoYou can hold a preference yet remain unbias. It’s biased because the author failed to acknowledge any of the conflicting test examples
- d35007 2y agoA preference is just a bias with a more positive connotation. The author acknowledges their bias in the title.
- Log_out_ 2y agoAfter exceptions comes acceptance of: https://fsharpforfunandprofit.com/rop/ https://fsharpforfunandprofit.com/rop/ of errorcases as normal program flow and wondering how you could ever accept those expensive packins and callbacks as normal behavior. https://vimeo.com/113707214 https://vimeo.com/113707214 C# is pretty far along wben it comes to allowing this style of programming, that actually should be in every language core.
- lifthrasiir 2y agoI originally only wanted to point out that the cited benchmark doesn't actually run the same thing for both cases (`do_fib_throws(...) + do_fib_throws(...)` has no defined call order), but then looked at the assembly and noticed that they are very differently structured. It turned out that GCC only recognized `do_fib_throws` to be eligible for tail calls and did some more inlining, and putting `noinline` and `optimize("no-optimize-sibling-calls")` attributes reduced the gap to a more believable level (~50%). As tail calls are highly sensitive to the exact call sequence, this benchmark is not suitable for the claim without detailed analyses. Yes, result types may result in a worse branch prediction among others. But that is rarely the primary performance issue caused by them, as you would expect the "unexpected" branch to be rarely taken anyway. The actual performance issue simply comes from the fact that it uses a more complex code in the typical path, so it may confuse the less sophisticated optimizer and prevent potential optimizations possible just like above.
- SuperV1234 2y agoThis article feels like someone trying to find arguments and justifications in favour of an existing opinion/bias. > Compare this to functional-style errors, where error handling is manual and super tedious. You have to explicitly check if the return value is an error and propagate it. No, you don't. You can easily convert a functional style error into an exception on the call site explicitly (e.g. `std::optional<T>::value` in C++, or `.unwrap()` in Rust), propagate via language features (e.g. `?` in Rust) or library abstractions such as monadic operations. The fact that you explicitly have to check the return value is a massive win in readability and ensuring that the "error case" was considered by the caller. That is paramount for the robustness of any codebase. > But there’s much more: Allocations can fail, the stack can overflow, and arithmetic operations can overflow. Without throwing exceptions, these failure modes are often simply hidden. Turns out there is a good use case for exceptions: exceptional and rare errors that cannot be reasonably handled in the immediate vicinity of the call site. Exceptions and error types can and should coexist nicely. > The classic example is syscalls, which usually follow C conventions. Yes, C error handling from decades ago sucks at providing information and context for the source of the errors. This is not an intrinsic issue with error return types, it's just another thing that sucks about C and is the way it is because it's old. > We parse an int somewhere, and an `IntErrorKind::InvalidDigit` bubbles up at the user. How is that a bad thing? Either the user provided the string that should have been parsed, therefore it's useful information for them to know why it failed to parse, or they don't care much, and they can explicitly decide to convert the error into an exception and propagate it upwards. Again, it forces the user to think about the error case, which is excellent! > The following examples use C++ code, which allows us to compare both versions like for like: [...] Now show a benchmark where the error rate is 50% or more.
- 01HNNWZ0MV43FF 2y agoIf the invalid int came from the network it could be confusing to tell the user
- gary_0 2y ago> C++ code ... Now show a benchmark where the error rate is 50% or more. C++'s policy of "exceptions should be exceptional" isn't a good answer to this either, and it introduces a lot of ugly edge cases when writing code. For instance, you have to make the judgment call of whether to handle commonly-expected errors as return values or exceptions (eg. checking if something exists, and returning it if it does). In many cases it is impossible to make the right call. For instance, a "file not found" exception is no big deal when your library is being used in a GUI app where user actions are relatively infrequent, but if your library is used in a high-traffic server or for batch processing, suddenly all those "file not found" exceptions are eating a lot of CPU. In general, you simply can't predict when users of your C++ code might find a case where a code path spams exceptions, tanking performance. This problem also exists in other exception-using languages like Python and C#[0], but these languages tend to be used for less performance-intensive purposes (especially Python) so it doesn't come up as much, and generally exceptions are used even for expected errors. [0] https://stackoverflow.com/questions/891217/how-expensive-are-exceptions-in-c https://stackoverflow.com/questions/891217/how-expensive-are... (C# exceptions are 30,000 times slower than return codes)
- LordHeini 2y agoI think exceptions are bad not because of the principle but because code is written by humans. We use Go in our company and every intern learning it complains about the error handling at first. Then they write a bunch of programs and those basically never crash. I had and intern write a somewhat complex service mangling large amounts of Geodata which was his first go service ever. That thing ran for years without crashing once (it died due to an hardware problem). The reason is simple: Golangs stupidly verbose error handling forces you to constantly think about the unhappy paths. Every function is littered with error handling and it makes you aware of possible problems and rigorously forces you to not forget them. Exceptions are the opposite: people do whatever and have someone else deal with the fallout later. This leads to people not thinking about errors and then someone using catch(all)-> doNothing kind of "solutions". On top of that exception based error handling is what i call transparent (like in invisible). So you can write a function using some other function without being aware the there could be exceptions thrown (unless your Language uses checked exceptions like Java). One can not possibly know every detail and remember the constantly. So if something goes wrong, you end up with super deep stack traces at unexpected places. This is unlike Go code where the error usually is handled at the earliest point possible with (hopefully) reason and contingency.
- ckcheng 2y ago> Golangs stupidly verbose error handling forces you to constantly think about the unhappy paths. Every function is littered with error handling and it makes you aware of possible problems and rigorously forces you to not forget them. That reminds me of this [1]: > However, if you are going to make a lot of state changes, having them all happen inline does have advantages; you should be made constantly aware of the full horror of what you are doing. When it gets to be too much to take, figure out how to factor blocks out into pure functions (and don't let them slide back into impurity!). [1]: https://cbarrete.com/carmack.html https://cbarrete.com/carmack.html
- kgeist 2y agoWe had one case when a developer realized his algorithm had a severe reliability problem when he realized he had no idea how to handle one specific error returned by one of the called functions. I remember us discussing that if it was written in PHP, we'd completely miss the problem (we were in the process of switching from PHP go Go), because with exceptions, it wouldn't be as obvious.
- darthrupert 2y agoI think Roc has the perfect balance. It allows ignoring the errors but also allows you to structure the code in such a way that you're forced to handle all error cases.
- ameliaquining 2y agoAs a counterargument, here's a good blog post (written for a Rust-centric audience) that gets into the technical downsides of exceptions (not the "error handling should/shouldn't be noisy" debate that's fundamentally subjective and unresolvable): https://smallcultfollowing.com/babysteps/blog/2024/05/02/unwind-considered-harmful/ https://smallcultfollowing.com/babysteps/blog/2024/05/02/unw...
- AtlasBarfed 2y agoGo's errors were a response to exceptions in Java. Java exceptions failed in several key ways: 1) exception catch sections had no lexical access to variables declared inside the try {} block. This was papered over a bit with some resource auto-closing syntax, but fundamentally an error handling section of code should be able to introspect the state of variables in the processing where the error occurred. YES you can move the variable declarations out of the try {} block but that is an annoyance, especially if you are expanding error handling to be robust, you must constantly move more and more local variables out of the block that they lexically and semantically belong to. 2) the try {} block imposes a big cost to casual error handling on a method invocation: it will force an indentation (which consumes precious horizontal real estate and may force excessive breakup of what should be a compact set of processing to more sub-functions), it involves a visually polluting keyword with the catch sections. Some syntax like sortMyJunk() :EXCEPTION IndexOverflowException ? handleOverflowError() :EXCEPTION OutOfMemoryError ? sendPionToMicrocenter() would have been really nice in java, it would have enabled more compact code that emphasizes the mainline processing, while visually placing the hopefully less common error processing off to the left or to an indentation. try {} should have been reserved (along with #1) for truly complicated exception flow handling. Perhaps Visual Basic had good ideas with error handling blocks occurring after labels, although my recollection and experience with VB wasn't deep: I don't remember if onError blocks had lexical access to local variable state.