9 ms·
I really, really dislike exceptions. Unchecked exceptions don't tell the caller something might go wrong. Fine for Python, where strong guarantees aren't a thi
by adimitrov 6y ago
I really, really dislike exceptions.
Unchecked exceptions don't tell the caller something might go wrong. Fine for Python, where strong guarantees aren't a thing anyway, but any statically typed language cannot be content with essentially adding bottom to every single type.
Checked exceptions have failed, or at least I haven't seen anybody fix their issues. They proliferate spurious exception types in interfaces. They are inflexible, as they usually can't be generic. They suck at typing error cases for higher order functions. They're big heavy and expensive, so can't be used for hot code paths. They're exceptions but more often than not you want to signal expected failure...
The list goes on...
- joseluisq 6y agoYeah, I dislike also exceptions. I'm more fan of Rust error handling for example via `Result<T, E>`. I believe that make a distinction between recoverable and unrecoverable errors is key.
- indiv0 6y agoI see a lot of people espousing this viewpoint and I just don't get it. What's the big difference between the way Java does it and the way Rust does it? In Java, unchecked exceptions are similar to Rust's panics. Sure your less inclined to catch panics than you might be to catching unchecked exceptions, but you can do it in either. Though perhaps Java's Error is closer to a rust panic. Checked exceptions are just like the Result type IMO. The discoverability of the error surface is the same, you just don't get the nice pattern matching and sum types to handle/represent it. What I'm getting at is that I'm a fan of the way Rust does error handling, but I don't get _why_ it feels different to the way Java does it. On the surface they're the same, but I loathe the Java approach. Maybe it's just whatever the opposite of rose-tinted glasses is.
- jcranmer 6y ago> What I'm getting at is that I'm a fan of the way Rust does error handling, but I don't get _why_ it feels different to the way Java does it. It's because of the way the exception information gets annotated. In Rust, exception information is part of the return type, so you can handle potentially-exceptional functions using mechanisms that are generic over any return type. For example, Iterator::map lets you call every function that might fail, and gather the results as failures, even if you don't fail-fast. But in Java, the exception information is passed in a separate part of the type signature, so generic functions have to be written differently to also handle functions that might fail.
- ddek 6y agoAs you say, the API feels similar. However, result types have many benefits over exceptions, with a few drawbacks. With a result type, in most languages, you can't just ignore failure. You must handle it, if you want to access the value. This prevents the common Java bug where a checked exception is caught but not handled. In rust, you must handle the error, propagate the error, or panic and fail fatally. Results don't abuse a runtime either. The exception process is slow - stack traces must be generated and the runtime itself needs to manually find the catch block responsible. Returning a result doesn't find this problem. Checked exceptions are fine for exceptional cases. At risk of sounding facetious, that's why they're called exceptions. But there's an important distinction that we need to make: logic failures are not exceptional. If you expect an operation may produce an error, you should handle it or crash the program. You should do this without incurring a performance hit, and using reliable and transparent primitives. Result types achieve both of these criteria, checked exceptions neither. The drawback to results is mainly the loss of data. Generating a stack trace is expensive, but often that's a worthwhile cost. Exceptions could be more versatile, in that any part of your code can throw any type of exception, but a result can only contain a specific type of error. That's not always a pro, though. So in general, checked exceptions are pretty awful for expressing normal failure cases, such as not finding a record in a database. They aren't great for anticipated errors, such as not finding a file, because of the chance a programmer might allow this to silently fail. Runtime exceptions are highly valuable, however. [1] - A decent SO post on the topic: https://stackoverflow.com/questions/613954/the-case-against-checked-exceptions https://stackoverflow.com/questions/613954/the-case-against-...
- wtetzner 6y ago> This prevents the common Java bug where a checked exception is caught but not handled. How does the Rust way avoid this? You can just as easily match on an error type with a catch-all case and ignore the error.
- Fellshard 6y agoBut you still acknowledge the presence of and explicitly supply a response to the failure case.
- adimitrov 6y agoTry to stick a function that may fail into a higher order function. The best example would be .map on streams/collections. You can only do it with unchecked exceptions in Java. In Rust, you can transparently do it with iterators and the result type. So given xs: [A] and f: A -> Result<A, E>, it is trivial to typesafely get Result<[A], E> by xs.map(f) where map: forall T. [T] -> (T -> S) -> [S]. This is outright impossible with Java. You have to circumvent the type system, or emulate Rust's approach.
- BoorishBears 6y agoMaybe I'm missing something but why does this not work with Java? Is it the fact you would put your xs.map call in a mapper or something? Because I know for sure you can do it in Kotlin: https://github.com/michaelbull/kotlin-result https://github.com/michaelbull/kotlin-result But I don't see reified generics as a requirement for what you describe, and that's the main Kotlin-only feature I see being used
- ben-schaaf 6y agoKotlin doesn't have checked exceptions, so being able to do it in Kotin is irrelevant.
- BoorishBears 6y agoWhat? a) I asked how is it not possible in Java. The Kotlin library is just an example of something I use, it's not relying anything impossible in Java as far as I can tell... like I said reified generics don't change what the calls would look like, just what the implementation looks like b) What do checked exceptions have using Result? The whole point is you use Result monads in all your code instead of exceptions. When interfacing with legacy code you wrap any exceptions in a Result class as well.
- adimitrov 6y agoThe idiomatic error handling mechanism of Java is the checked exception. This mechanism does not work with higher order functions. You can emulate Rust's approach with Java, by creating what is essentially a sum type like Result. You'd have to enforce that any access to its content must also handle the error case, and I don't really know how to do that generally. There are various hacks, like having a bespoke sum type for that particular operation that twrows a particular exception on access to its content. But that gets really old, really fast.
- derriz 6y agoWith checked exceptions it's not clear how control can pass into the catch block (using Java terminology). The fact that a method can throw a particular exception is not visible at the call site so unless you examine the declaration or definition of every method call within the try block, you cannot be sure from where control can jump to the catch block. And if there are multiple places within the try block where the caught checked exception can be thrown, then basically the catch block can do nothing sensible with respect to recovery or even guaranteeing anything about the current state of the world once you're in the catch block. I've seen some horribly ugly patterns used in an attempt to deal with this - basically you need to explicitly record progress within the try block which can be tested in the catch block. And whatever about Java - at least the design decision hasn't completely poisoned the subsequent development of the language. Whenever I use C++, it feels like a massive amount of the complexity, wizardry and arcane/complex patterns required to write "safe" C++ would not be necessary if they hadn't added exceptions to the language early on.
- smichel17 6y agoWhat about this middle ground? These are my thoughts, very curious whether there are languages doing this & if so, what the real world problems are. 1. Statically typed. 2. Unchecked exceptions by default. IE, implicit bottom on every type. 3. Optional checked exceptions. Implementation could be `checked` keyword (e.g. `checked fun foo() throws XyzError {}`), or a generic type, or something else depending on the language. In order to mark something as checked, it must catch all exceptions from unchecked values it touches. 4. The compiler infers exceptions types when possible. So, if a regular unchecked function (`fun bar() {}`) only calls checked functions (foo from above), the compiler will carry the information about what can be thrown (XyzError) up the chain, even if there is no explicit signature. This way if another function (`checked fun baz() {}`) calls bar(), then it only has to catch or declare the XyzException, rather than all exceptions generically. Finally, and to tie this all together: as a matter of style, reserve exceptions for actual unexpected errors. To use an example from downthread, a JSON parsing library should not be throwing parsing errors, it should be returning a Maybe<Parsed, Error>. Why? Because the library cannot determine whether the error is unexpected. If you're using it to parse user input, then yes, you're going to frequently encounter invalid data, and this should be part of your normal control flow, not something that can crash your application at runtime (you may want to log the event, maybe with a stack trace, but without interrupting normal operations). On the other hand, say you're parsing JSON from your database, (also, why are storing json in your db? But bear with me..). And invalid json means data corruption, which means your db is fubar. Then, it's fine to throw, because in that case you're operating on state that you assumed to be impossible, and your program behavior is now undefined. The key point here is that exceptions are useful as a safety valve for early termination in the case of invalid assumptions. That's a subset of all error handling. If you try to force all error handling into the same paradigm and the same level of reliability, you're going to have awkward edge cases.
- smichel17 6y agoReply to self: barrkel's comment upthread describes the premises I was working off of quite elegantly: https://news.ycombinator.com/item?id=25260631 https://news.ycombinator.com/item?id=25260631
- MaxBarraclough 6y agoYou might enjoy this 2005 blog post from the great Raymond Chen titled Cleaner, more elegant, and harder to recognize. https://devblogs.microsoft.com/oldnewthing/20050114-00/?p=36693 https://devblogs.microsoft.com/oldnewthing/20050114-00/?p=36...
- alquemist 6y agoI have to agree with the author. It is extraordinarily difficult to see the difference between bad exception-based code and not-bad exception-based code. In particular, the example of bad vs not-bad doesn't show substantive differences between the two versions. // bad NotifyIcon CreateNotifyIcon() { NotifyIcon icon = new NotifyIcon(); icon.Text = "Blah blah blah"; icon.Visible = true; icon.Icon = new Icon(GetType(), "cool.ico"); return icon; } // not-bad NotifyIcon CreateNotifyIcon() { NotifyIcon icon = new NotifyIcon(); icon.Text = "Blah blah blah"; icon.Icon = new Icon(GetType(), "cool.ico"); icon.Visible = true; return icon; } What is the problem with the first version and how is the second version fixing it?
- MaxBarraclough 6y agoI agree the article might have been more explicit there. If I'm understanding correctly, it's all about possibility of this statement throwing an exception, as for example the cool.ico file might not be found or might be corrupt: icon.Icon = new Icon(GetType(), "cool.ico"); I think the point is that the 'bad' version can make the icon visible in the UI and then throw an exception. The 'not bad' version has a more transactional flavour: if the aforementioned statement throws, then, because of the better ordering, the 'not bad' version doesn't make the unfinished icon object visible, it just bails out with the exception having made no change to the UI, and the unfinished NotifyIcon instance gets garbage-collected. I presume that the real-world code included some extra machinery to hook it up to the existing UI objects, omitted for brevity in the example. It's a bit confusing as it looks rather like it's just building up and returning a NotifyIcon instance for the caller to make use of, but I think the icon.Visible = true; is meant to represent truly making the icon visible in the UI.
- barrkel 6y agoSpurious exception specifications are the flip side of avoiding not telling the caller something might go wrong. It's a fundamental tension and is unavoidable. Failure modes are an abstraction violation; they're a function of implementation. That's what makes checked exceptions not work, at the end of the day. Information-carrying exceptions reveal implementation details. So a module author must decide between hiding details and wrapping everything up in module-specific exceptions that user code can't actually use to make decisions most of the time, or expose implementation details that turn into a versioning problem over time. There's roughly two situations for error handling: near the leaf of the call tree, where you have enough context to deal with an error, and need to switch on error type and take compensating action; or near the root of the call tree, in the main loop, where you log errors and terminate requests etc. in a generic way (e.g. 500 response). Exceptions aren't ideal for the first situation but are great for the second. Error codes are adequate for the first situation - monadic error types (Result<T>, Either) are a bit better - but suck horribly for the second, because you need to manually unwind, writing boilerplate that should be automatic. And at the limit, error types are isomorphic to checked exceptions, with the same problems, and more - error types introduce an aggregation problem, where multiple errors need to be joined together. You can still get that with exceptions too but it usually requires parallelism.
- karatinversion 6y ago> There's roughly two situations for error handling: near the leaf of the call tree, where you have enough context to deal with an error, and need to switch on error type and take compensating action; or near the root of the call tree, in the main loop, where you log errors and terminate requests etc. in a generic way (e.g. 500 response). There is actually a third case: in library code which calls other code which may fail. Take java.io.BufferedReader - to be usable, it has to be at a level of abstraction where it cannot deal with any errors the underlying Reader may throw; but the code using BufferedReader will have provided it with its underlying Reader, and will have a good idea of what errors are reasonable to expect from it. The reason java's checked exceptions are so bad is that they cannot (or could not, before generics, and hence in most of the standard library do not) serve this use case, leading to checked exceptions that one really can't do anything with.
- karatinversion 6y agoMy hot take on this is that java checked exceptions are bad because their design predates java generics. Because of the lack of generics, authors of packages such as java.io had to create god-types of exceptions for their general interfaces to throw. A good example is public int java.io.Reader.read() throws IOException As I see it, the purpose of checked exceptions was to allow declaring expected failure modes in the function signature, so that the programmer (and the compiler!) could check against them - but when my StringReader declares itself capable of throwing an SSLException (subclass of IOException), this benefit is lost. Instead, I must rely on other sources to determine which errors may actually occur, and which I can't do anything about - and the latter I must swallow or pollute all of my package's function signatures with. If the Reader interface had instead been generic java.io.Reader<T extends Throwable> read() could be declared as public int read() throws T This would rescue much, and is something that could be done in modern java; but by the time generics were introduced, all the core packages like java.io were written, and the patterns for how to deal with checked exceptions were set.
- renox 6y agoWalter Bright (D's author) said something like: who's going to check the error returned by the log functions? I would add if you want to be correct then the addition signature should be (IntX, IntX) -> Either<IntX,Error>. Are you going to check also the return value of every arithmetic operation? For me potentials errors are everywhere, so exception aren't bad, even if they make writing 'exception safe' code hard. That's why I was quite interested in the Vale language which claims to improve RAII: https://vale.dev/blog/next-steps-raii https://vale.dev/blog/next-steps-raii