3 ms·
It's interesting to see modern languages tackling checked error handling. Particularly in the light that Java's checked exceptions are widely considered to have
by zmmmmm 8y ago
It's interesting to see modern languages tackling checked error handling. Particularly in the light that Java's checked exceptions are widely considered to have been a failure. Yet I wonder if Java had better support for things like union types etc. if it would have turned out differently. After all, it did offer at least potential to achieve what everyone is after: you can write a piece of code and know for sure that you handled every possible type of error.
The most painful aspect of Java's (original) exceptions was how the only mechanism to handle multiple errors as a single case was using inheritance in the exception hierarchy. The result was that they propagated through interfaces and broke downstream code and it became pretty much impossible to design and maintain a stable API without wrapping exceptions excessively which in turn destroys the utility of the exception hierarchy, making the whole exercise futile.
The article seems a little superficial though. Immediately exiting with 'panic' does not seem like a real error handling approach for any realistic application. It doesn't show how you could handle different types of errors (eg: file not found vs I/O error reading file) with different cases. The case it does present is easily handled elegantly by nearly any error handling design.
- int_19h 8y agoI think the biggest problem with Java exception specs is that they're not composable. In other words, I can't define a method X that calls another method Y - potentially via dynamic dispatch - and has an exception specification of "whatever Y throws, plus E". This manifests almost immediately. For example, suppose you wanted to implement a key/value database with disk storage. The logical core interface for it would be java.util.Map, except for the fact that its methods are declared as non-throwing; thus, you cannot propagate IOException from the underlying storage layer, unless you wrap it into unchecked RuntimeException. Conversely, java.lang.Appendable is a very generic interface for appending characters or sequences of characters to something. But because its designers knew that some of its implementations would be backed by I/O, all mutating methods in that interface are declared as throwing IOException. Thus, any generic code that tries to work with Appendable has to always assue that it can throw, and has to declare itself as throwing the same exception to propagate. And now the user of that generic code passes a no-throw implementation of Appendable to it, but still has to "handle" IOException that they know will never actually happen. Or come up with some convoluted scheme, like java.util.Formatter does. This one wraps Appendable, but swallows all I/O exceptions to avoid having to propagate them in the most common case, where the wrapped Appendable is a StringBuilder. But since there are other possibilities, it still has to expose some API to examine such exceptions, which it does by providing a method on Formatter that returns the last swallowed exception that was thrown by Appendable. Of course, now you have to remember to actually check it, and you will silently get an incorrect result if you do not, which is worse than unchecked exceptions in the same scenario. This all gets especially bad one you start working with HOFs, because their contracts are largely defined in terms of the behavior of functions that are passed to them. The old Java lambda proposal by Neal Gafter noted this, and proposed extending the syntax for generics to cover exception specifications (via union typing, indeed). In the end, they added UncheckedIOException to the standard library, specifically so that stuff like Stream.map() can be implemented on top of files, and propagate exceptions, without declaring them.
- zmmmmm 8y agoThat is a very nice summary! Some of these tensions seem unresolvable though. At some level if I want to toss a Map around everywhere and be able to transparently back it with a database, I'm going to have to accept that operations might fail. I can't have it both ways. In the end, the abstraction is "leaky". I don't really see how Rust or anything else can make that go away. I feel like the closest I can get to have my cake and eat it too is by having the compiler transparently infer all the exceptions / errors that can be thrown by the calls I am making (like type inference for variables), but leave them out of the type signatures. Then I can opt in to enforcing the checking of them when I want to, so something like check { doSomething() } catch(...) { // every error now has to be handled .... } I guess this is something like what Rust is achieving through the ? operator.
- steveklabnik 8y agoIn rust, what you can do is define an error type as an enum, which holds the various kind of inner errors. Then you’re just a From implementation (which is very simple and macros exist to generate it) away from ? working on any of them, and it composes them together.
- int_19h 8y agoIn the case of Map, since some maps fail and others don't, and those that fail can fail with different errors, it would be reasonable to express in in the type itself - i.e. instead of Map<T>, you have Map<T, E> (and then you also need union types to properly express E being a list of exceptions, and a unit type to express an empty list). Then any code that handles generic maps can also be generic wrt E, propagating whatever is thrown, and possibly adding its own errors, or catching some specific ones. And, conversely, you could only accept Map<T, Unit> for code that expects non-throwing maps, and then you don't need to catch or propagate anything in that code. Add defaulted generic parameters for convenience - e.g. Map<T, E=Unit> - and now API clients don't have to care about E if they don't want to, and are happy with things as they are. Alternatively, you can just say that map is something that never throws, by definition. But, of course, that reduces the utility of the interface, by making it less generic than it could be. In some cases - especially with HOFs - the reduction in utility can be so substantial that it negates any gains from code reuse.