10 ms·
Exceptions are starting to feel like a legacy programming paradigm to me. Rust & Go have, at least in a practical programming context, shown that errors-as-valu
by mindwok 4y ago
Exceptions are starting to feel like a legacy programming paradigm to me. Rust & Go have, at least in a practical programming context, shown that errors-as-values has far less footguns and encourages better error handling practices than exceptions, which often are treated as an afterthought or end up being abused like in this post.
Whenever I'm writing Python or Java I can't help but feel anxious about calling a function and having absolutely no idea if or what exceptions it might throw, and then having to resort to digging through documentation or class hierarchies to figure it out.
- lolinder 4y agoI think what Java got wrong was allowing catching unchecked exceptions. If Java had only allowed recovering from checked exceptions, it would have been a very similar experience to Rust's Result and panic. Instead, the trend was to avoid using checked exceptions at all, which perpetuated the crazy situation we're in where library code can suddenly abort and the author shrugs and says "shoulda read the docs".
- lmm 4y ago> I think what Java got wrong was allowing catching unchecked exceptions. If Java had only allowed recovering from checked exceptions, it would have been a very similar experience to Rust's Result and panic. > Instead, the trend was to avoid using checked exceptions at all Java's type system was far too weak for that to be at all practical. Early Java not only didn't have first-class functions, it didn't even have generics. Even today checked exceptions are horribly cumbersome - for example, it's simply not possible to write a wrapper function that accepts a function and calls it, and throws the same set of checked exceptions that the inner function does. If Java had had proper ML-style types from day 1, like Rust does, then it could have done "errors are values" in a practical way. But forcing people to use checked exceptions would have resulted in either C-style errno codes, or the whole language collapsing under its own weight.
- gpderetta 4y agoYes, checked exceptions with a proper type system and abstraction facilities would be completely equivalent to Result<T,E>, except built into the language.
- twic 4y ago> it's simply not possible to write a wrapper function that accepts a function and calls it, and throws the same set of checked exceptions that the inner function does It is possible, and straightforward, to do that if the set of checked exceptions is of a statically known size, and the wrapper function can propagate the exceptions rather than catch and rethrow them. In my experience, that is a very common situation (although not universal). If you want to catch and rethrow, that can also be done, but it requires an evil hack, and the wrapper will need to take a reified type for the exception.
- lolinder 4y agoThat's fair. Lack of inference is also a huge problem with Java's checked exceptions. My point is less that Java could have done this differently with the other type choices that they made and more that it's a shame that Java's implementation of checked exceptions has poisoned the concept for so many. I think a language with strong type inference and a checked exception mechanism would be better than Rust's Result type because you get the stack trace.
- groestl 4y ago> where library code can suddenly abort and the author shrugs and says "shoulda read the docs". In see no difference. Except for the fact that you don't have the stacktrace (in Go), and unroll the Stack manually. In any case, there'll be a generic catch (whatever you call it) on the top level of your daemon to recover from the failed request and handle the next.
- gthrowaway52 4y agoIn Go the programmer is forced to make a decision on what to do with the error. Common patterns include (A) return an annotated error, (B) log it and continue, (C) retry, (D) aggregate the errors in some way. If you believe the only way to handle an error is (A), then Go's design makes no sense.
- groestl 4y agoYou seem to believe options B to D are not available to programmers in languages with exceptions. The real value comes from making option E much more unlikely: ignoring both the result and error value altogether, because you relied on the side effect of the function you called.
- nindalf 4y agoYou might like how Rust does error handling. Rather than a function returning a triple of (val, error) where the error can be ignored, functions return Result<T, E>. If you want to get the T, you must write code that handles both possibilities. If your function instead wants the T and propagate the E upwards if it exists, you can do that with one character - “?”
- groestl 4y agoI indeed like Rust's approach a lot more than Go's. What I like less still is that it gives the impression that it's even possible to define functions that cannot fail. This is not true. One just has to look at how runtimes deal with stack overflow errors to see how the good old Java RuntimeException creeps in in various forms (e.g. panics) because checked exceptions and it's recent incarnation as error values are a leaky abstraction.
- gpderetta 4y agoCatching unchecked exceptions at module boundaries is perfectly fine and as far as I know (which admittedly isn't much) both Go and Rust in fact allow doing exactly that.
- vbezhenar 4y agoJava got wrong with the concept of checked exceptions. They're not needed. Python, C++ or JavaScript exceptions are totally fine. And checked exceptions bring nothing but issues. The only thing that I'd add to the unchecked exceptions is noexcept with compile-time checking. Something along the lines: 1. You can declare method as `nothrows`. Compiler will ensure that no exceptions are thrown (`java.lang.Error` can still be thrown, but you're not supposed to deal with it in any way in most code). 2. You can declare method as `throws Exception1, Exception2`. Compiler will ensure that only subclasses of those exceptions are thrown from this method. 3. You can omit any declaration. In this case compiler will compute list of possible exceptions implicitly (it's a union of all exceptions thrown by all called methods). So basically it'll allow to: document list of thrown exceptions and it'll allow to statically check that other exceptions are not thrown, so this documentation is compiler-checked. Of course this idea needs battle testing, but I think that I'd like it. You can either opt-in and write code documenting all thrown exceptions (which is good for libraries) or you can opt-out and write simple code without bothering with exceptions (which is good for applications). Also there should be proper support for generic exceptions, so I can write Function<T, R, E> and E would be of type containing union of all thrown exceptions. That's required for example for moving exception signatures from lambdas in functional collections.
- pjmlp 4y agoJava always gets the blame, yet the concept of checked exceptions was introduced by CLU, adopted by Modula-3 and C++, before it came to Java. And even though C++ dropped exception specifications, they still kept the difference between might throw anything or doesn't throw at all, and there is also the paper to reintroduce them Swift style.
- vbezhenar 4y agoThose are not industry languages and Java was known to borrow only well developed and widely adopted features. For example it took like 20 years to add lambdas which were in lisp 60 years ago. Checked exceptions are a sore exception to this rule.
- deleted 4y ago[deleted]
- hirrolot 4y agoRust do still has exceptions, they're just called "results". You can't really have values as exceptions unless you plug a lot of ad-hoc constructs into your language so that they eventually become similar to exceptions [1]: > Add syntactic sugar for working with the Result type which models common exception handling constructs. The whole error handling story with Rust can be summarized as "we have results but want to have exceptions". [1] https://rust-lang.github.io/rfcs/0243-trait-based-exception-handling.html https://rust-lang.github.io/rfcs/0243-trait-based-exception-...
- dsQTbR7Y5mRHnZv 4y agoErrors in rust are not exceptions, they're data contained within an `Either` monad (Result) Additionally, on top of the linked RFC being nearly 9 years old it doesn't at all indicate "we want to have exceptions". The ? operator allows propogating the errors if they can't be immediately handled (similar to monadic `do` notation, or early returns)
- pwdisswordfish9 4y agoJust because Rust people are in denial about it doesn't make them not exceptions. The Either/Result monad is isomorphic to checked exceptions, with any differences being pretty much just syntax.
- jcparkyn 4y agoIt makes generics and higher-order functions much more elegant. Any function of `(a->t)->t`(or many other similar signatures) will automatically be able to return the correct error type if t is a Result<>. Compare this to checked exceptions, where (even in an ideal world) you'd need a separate type parameter for the error type, plus a bunch of extra language features and syntax to make it work. And then what happens if you want to use the same function with _no_ error type? For a concrete example, try using Java's `stream().map()` with checked exceptions. Of course there are implementations of checked exceptions which are much closer to Result than Java's implementation, and in those cases I would agree with you.
- tsimionescu 4y agoAt least for Go (haven't used Rust), I entirely disagree. After ~3 years of professional programming with the language, `if err != nil` is still constantly annoying me when writing code, but especially and most importantly, when reviewing code. Not to mention, Go has proven conclusively to me that exceptions are exactly the right pattern for error propagation - the 99.9% pattern is "function returns error with message; callers add context; top-level caller aborts and logs + returns message to user, or sometimes retries". This is exactly how exceptions work out of the box, without the need to pollute all code between the error source and the top-level caller. If anything, I'd like to see a language seeking to add smarter information to exception stack traces - not just the function + code line, but also some information about local variables might be doable, and would supplant the one occasional gap between Go-style hand-built context and Java/Python auto-generated exceptions.
- xcdzvyn 4y agoThis is also how errors-as-values work in Rust. Functions that may fail return Result<T, E> - putting an ? at the end of a failable function call returns T on success, otherwise it propagates E. It reduces result, err := call() if err != nil { return err } to let result = call()?; Completely unobtrusive, but makes failure awareness an obligation.
- tsimionescu 4y agoI know about ? in Rust - the one thing that isn't very clear to me is how often it is enough. That is, with Go, it's quite typical to do something like: result, err := call() if err != nil { return fmt.Errorf("Error while trying to call: %v", err) } Essentially manually building a stack trace. If just doing "return err", you end up with calls to a REST service failing with messages like `couldn't parse "" as int` even in internal logs, which isn't helpful. With exceptions (in any language except C++), the stack trace is often decent enough. Does ? add any kind of context implicitly, or do you have to actually pattern match manually to add context?
- shirogane86x 4y ago
- rocqua 4y agoExceptions work well for 2 kinds of error handling: crash and log, and general retry and log. The second one is more cumbersome with return values if the possible errors originate deep in the stack. Beyond a simple retry, Exceptions are not specific enough for handling errors.
- TeMPOraL 4y agoExceptions are typically custom types - you can build an arbitrarily detailed exception taxonomy (typically branching off the language-standard one), and in most (all?) languages supporting exceptions, you can also give them arbitrary state. You really can't get more specific than that. If exceptions still feel "not specific enough for handling errors", perhaps it's because you're only thinking of most trivial examples, as given by 101 tutorials and people arguing against exception handling?
- rocqua 4y agoYou can build a separate exception for every possible raise site. But that is overly burdensome. Moreover, there is no guarantee that any (standard) library calls have an appropriately detailed taxonomy. You could, of course, wrap these library calls and catch exceptions so you can re-raise them yourself. But at that point you are losing the main advantage of exceptions over return values: the ability to separate the happy path from the error-handling path. To me, once I am building my own expansive taxonomy of exceptions, I am much happier using Optional, or Result/Either type return values.
- TeMPOraL 4y ago> You can build a separate exception for every possible raise site. Why would you want to do that? You don't do that with Result/Either. > To me, once I am building my own expansive taxonomy of exceptions, I am much happier using Optional, or Result/Either type return values. I may be missing something, because to me, this doesn't follow. Optional type is "result or nothing"; and with Result/Either type, you either use something generic (e.g. symbol, string), or go very specific (even if it's just one of the dozen different "newtype" names for symbol/string). To me, this choice with Result/Either is exactly equivalent to choosing an exception taxonomy. You're doing the same work either way.
- jayd16 4y agoHas it shown this? Are there solid metrics or is it simply because they're popular and it's not the idiomatic way to do things? My understanding is they still have panics anyhow.
- mindwok 4y agoI should have caveated that it's purely subjective. In my experience, developers write much more reliable and understandable code when they handle errors as values rather than as exceptions.
- MrBuddyCasino 4y agoThe Rust Result type combined with mandatory error handling leads to very robust programs. It is also easier to reason about, as there is no hidden (non-local) control flow. Exceptions are unergonomical and slow.
- dmitriid 4y ago> Exceptions are starting to feel like a legacy programming paradigm to me. Rust & Go have, at least in a practical programming context, shown that errors-as-values has far less footguns They don't really. Because they force you to handle errors even in code that couldn't care less about errors because that's the responsibility of a higher level up the stack. And worse, they assume that an exception is a valid reason to just stop program execution altogether (when it rarely is). See Erlang for how you should handle exceptions (you have a supervision tree with various restart strategies).
- dmitriid 4y agoJust remembered that Rust realised that manual handling of errors everywhere doesn't work, and introduced `try!` macro, and then the `?` operator. These are more practical (and good thing Rust isn't rigid at the expense of pragmatism). And of course since panics are not the answer to everything exceptional, there's now a `catch_unwind`, too
- jve 4y agoI long wished there was C# Intellisense that showed what kind of exceptions I may receive when calling particular function. At least from code it can infer from: - .NET library has documentation comments, with <exception> tags - It could look at my code and see what exceptions get thrown. - It could be clever enough to know that new might throw OutOfMemoryException - Clever enough to know checked arithmetic might throw OverflowException - Might understand where/what exceptions get caught and do not bubble up. - Smart enough to understand if value can possibly be null and properties are accessed, thus may result in ArgumentNullException. - As for 3rd party code, I don't know if .pdb provide enough info to know whether particular function call may throw particular exception? But it's all IL, so that should be enough to infer from 3rd party code what kind of exceptions I might encounter: // throw new ArgumentNullException("serviceProvider"); IL_0013: ldstr "serviceProvider" IL_0018: newobj instance void [mscorlib]System.ArgumentNullException::.ctor(string) IL_001d: throw Eh, what a dream.
- Genbox 4y agoWhat you are asking for is checked exceptions where the caller gets notified by the compiler that the function you call might throw one or more exceptions. However, it is worth taking it a step further and focus on "why the error occurred" instead of "an error has occurred". An example of this is Code Contracts. Like Intellisense, live code analysis with contracts would tell you "the method you call will throw <exception-type> with the input you give". Not only will that cover giving the user information about which error conditions that can arise, it will also give you the reason why.
- jve 4y agoWell, code contracts are unfortunately dead, not available in .NET Core. C# Anders Hejlsberg, C# designer has a word on checked exceptions for which I agree with him that we should somehow address it more efficiently: https://www.artima.com/articles/the-trouble-with-checked-exceptions https://www.artima.com/articles/the-trouble-with-checked-exc... Looks like what I'd like in the meantime is soft-checked-exceptions. Well, just for documentation purposes. And I'll leave the code for myself. I just want to take into consideration various failure modes and now I can just do generic error handling or waste too much time. Yes, what you are saying, live code analysis would be good enough not to screw up C# language itself with bad design decisions.
- knorker 4y agoNot so much legacy as specifically of the time period where Java was created. C++ doesn't do this (in the standard library, setting a good tone for idiomatic code). Older languages don't abuse the exception idea. Nor do newer ones. (Go is a bad example though, since it took the convenience of Java style exceptions away, but replaced them with nothing)
- commandlinefan 4y ago> feel anxious about calling a function and having absolutely no idea if or what exceptions it might throw You can try doing what most of my coworkers do and just not care.
- goto11 4y agoOn the other hand the Rust syntax have gradually moved closer to exceptions, now with the "?" operator which automatically unpack the result or propagates the error value. This feels awfully close to exceptions, even if it in theory is "just a value".
- mindwok 4y agoI think the main difference is that it forces the acknowledgement of the error at the point of the function call, which is not the case with exceptions.