4 ms·
Do you know of any blogs or writing on this that I could read? Naively, untyped exceptions in Swift always felt like an obvious type system loophole that we sh
by jkubicek 2y ago
Do you know of any blogs or writing on this that I could read?
Naively, untyped exceptions in Swift always felt like an obvious type system loophole that we should be closing as soon as possible.
I trust that the "typed exceptions are an antipattern" people know what they're talking about, but I really don't understand the reasoning behind that position.
- MBCook 2y agoI’ve heard that claim many times before. I’m a daily Java developer, and can provide a little perspective on what Java is like. Though I don’t know the exact specifics of the claim. In Java a given method either doesn’t throw any exceptions, or only throws the named ones and their children. So nothing, or maybe just InvailArgumentException (and its descendants). However there’s a lot of different kinds of exceptions. So very quickly you tend to end up with one of two things. Functions have a big list of possible exceptions (an A, or B, or C, or F, or G) or the far more common in company code: throws Exception. Don’t bother to list stuff, just say “I throw stuff”. Except, that’s all a lie. Because every function implicitly can throw RuntimeException and its children. Things like OutOfMemory or StackOverflow. So basically all code everywhere realistically has to be aware of exceptions. A lot of people get lazy and don’t bother to list the actual kinds of exceptions they throw or pick one that’s a runtime exception so they don’t have to add the ‘throws’ to the signature to every single method. And how do you solve calling a method with a checked exception? Well the right thing to do is use a try block, but so often people just add it to their own signature and pass the buck. So a lot of the benefits just sort of get lost. Swift now has three options: doesn’t throw, throws something, throws of type X. You can’t have multiple types. And you can’t pretend the exception doesn’t exist and just let it bubble up, you have to put a ‘try’ on the statement line to do that. As someone who works in both languages Swift’s approach does seem like an improvement. I’m not sure how big. But just in Swift, being able to explicitly state the one type of the exception that may be thrown seems like a very nice add for type safety and reducing boilerplate checks.
- koito17 2y agoMinor nitpick: OutOfMemoryError and StackOverflowError are subclasses of Error, not Exception. It is an anti-pattern to catch any Error subclasses since these are usually JVM-internal exceptions. Programs are firmly in undefined behavior territory when they e.g. catch an OutOfMemoryError and do nothing with it. This is why catching Throwable is considered a bad idea compared to Exception. In the majority of cases, there is no reasonable recovery from a thrown Error, unlike a thrown Exception. With that said, it is true RuntimeException and its subclasses are unchecked exceptions. Thankfully, those exceptions tend to be things like IllegalArgumentException, NullPointerException, etc., which are still pretty serious but not as bad as the two examples given.
- MBCook 2y agoAh that’s right. I’m used to RuntimeException since that’s what people subclass to get out of the checked exception mess. The exact hierarchy around rare things like OutOfMemory just isn’t something that ever comes up in day to day work so it’s easy to forget. Thanks for the correction.
- dwaite 2y agoThe ThreadDeath Error in particular is interesting because it is catchable but not "handle-able" - if you do not rethrow ThreadDeath in your exception handler, the JVM will do so for you. Despite Thread.stop() being deprecated, it is still used in things like application servers to deal with user code which is not completing in a specified timeframe.
- dwaite 2y ago> Except, that’s all a lie. Because every function implicitly can throw RuntimeException and its children. Things like OutOfMemory or StackOverflow. > So basically all code everywhere realistically has to be aware of exceptions. A lot of people get lazy and don’t bother to list the actual kinds of exceptions they throw or pick one that’s a runtime exception so they don’t have to add the ‘throws’ to the signature to every single method. Not just that, there are features like lambdas where the core language features were not designed with exceptions in mind. There are third party libraries which don't have allowances for proper failure cases. So you will have code adapted to wrap their exceptions into runtime exceptions for interoperability. Of course, there's no guarantee the code which did not provide an interface which allowed for exceptions will not have unexpected side-effects when runtime exceptions or errors are being thrown. > However there’s a lot of different kinds of exceptions. So very quickly you tend to end up with one of two things. Functions have a big list of possible exceptions (an A, or B, or C, or F, or G) or the far more common in company code: throws Exception. Don’t bother to list stuff, just say “I throw stuff”. Ideally, someone creating an API thinks about how the others utilizing code will accomplish recovery or graceful failures, and craft an API to account for this. Unfortunately, the errors are often handed up, which really only accounts for broad failure behavior and corresponding cleanup (such as failing the corresponding HTTP request with a generic 500 error). The heavy use of adapter patterns in Java means that the errors being handed up may be specific to the configuration, such as failures coming from the network layer of a database backend. It is crucial for the adapter interfaces to think about failure scenarios and recovery, but it unfortunately doesn't happen often very often. So in most cases, Java should just throw Exception, because the only way a developer can really attempt recovery is to break encapsulation and operate based on knowledge of the inner workings of the object they were handed.
- kccqzy 2y agoIt's not a type system loophole in the sense that the type system is supposed to contain subtyping as a feature. In Java you could be unhelpful and pass every argument as type Object. (Of course the language allows you to cast the type afterwards.) That's not the fault of the type system it's the people using it. Do you hear people proposing to ban the use of Object in function arguments? No? Because it's a style issue not a type system issue.
- MBCook 2y agoYou’re right but I think that’s pretty self-defeating so people wouldn’t really do it. But there’s a pretty good argument that the way exceptions are implemented in Java makes the easiest thing to do simply be to start tacking “throws Exception“ on the end of tons of methods once you start to have trouble. Everywhere I’ve worked you start to see that in chunks of the code base. You don’t have to. It’s certainly avoidable. But as things get more and more complicated people will just opt for that to save themselves time. Java’s design unintentionally encourage it when the going gets rough. Even when using an IDE that can add the correct types to the list for you, that’s what people reach for.
- Tainnor 2y agoIn my opinion, exceptions should be for really exceptional situations - things you can't realistically expect and/or which just signal a programming error (e.g. a precondition being violated). They aren't usually recoverable locally and should bubble up the stack, so you can handle them at some boundary (e.g. so you can serve up a 500 page to a request without crashing the entire server for everyone). For things that are expected to go wrong sometimes, algebraic data types (including Result types) are better. They are part of the type system, so everything you can do with types, you can do with them, including making functions parametric over whether they return an "error" or not. Exceptions need extra mechanisms so that functions can be parametric over them (e.g. Swift has "rethrows", Java has... nothing). They aren't really composable with anything else.
- svieira 2y agoI haven't heard anyone anywhere say that "typed exceptions are an antipattern" just "typed exceptions _as implemented by Java_ are an antipattern". The general problem is well set forth by Anders Hejlsberg, Bill Venners and Bruce Eckel in _The Trouble With Checked Exceptions_ [1]. Briefly, typed exceptions in Java are not genericizable. Everything must be known at compile time. That is to say, you cannot write: interface ThrowingRunnable<E extends Throwable> { void run() throws E; } Instead, the best you could do is manual multiplication of interfaces: interface AThrowingRunnable { void run() throws ExceptionA; } interface BThrowingRunnable { void run() throws ExceptionB; } // ... snip thousands more ... Or give up and erase all the type information by saying `throws Exception` or `throws Throwable`. Genericizing throws in particular was tried in Midori [2] and worked out really well (by report). In addition, several less-than-completely-obscure languages are starting to experiment with the algebra of effects in general (as opposed to error handling in particular). Pony [3], OCaml [4], and others are experimenting with bringing what Koka [5] (among others) to the masses. [1]: https://www.artima.com/articles/the-trouble-with-checked-exceptions https://www.artima.com/articles/the-trouble-with-checked-exc... [2]: https://joeduffyblog.com/2016/02/07/the-error-model/ https://joeduffyblog.com/2016/02/07/the-error-model/ [3]: https://www.ponylang.io/ https://www.ponylang.io/ [4]: https://ocaml.org/manual/5.2/effects.html https://ocaml.org/manual/5.2/effects.html [5]: https://koka-lang.github.io/koka/doc/index.html https://koka-lang.github.io/koka/doc/index.html
- sc22 2y agoYou _can_ write in Java that very interface you claimed could not be written. interface ThrowingRunnable<E extends Throwable> { void run() throws E; } Only, hardly anybody writes it that way, so people forget that it's possible. Why don't people use exception polymorphism in that way? I am not sure, but I believe it would make code too verbose, since every function containing a virtual method call to something that might throw would have to be parameterized that way.
- Someone 2y ago> I trust that the "typed exceptions are an antipattern" people know what they're talking about, but I really don't understand the reasoning behind that position. The reasoning is that they force a contract upon implementations that they may not be able to implement in spirit. Let’s say a library defines a container interface that says a method can throw a NoSuchElementException. Now, I implement that interface to implement a disk backed container. What do I do when that code encounters an IOException? Swallow it (very bad), throw an NoSuchElementException, possibly wrapping the IOException inside it, if possible (highly confusing, as now the class changed the meaning of NoSuchElementException), or throw an unchecked exception that wraps it instead? Most Java code would pick that last option, and you end up with a nice interface that says that a call can throw NoSuchElementException, but callers must be prepared to get a RuntimeException that wraps any other kind of exception. That makes the implementation adhere to the interface, but only in name. (Note that the writers of the interface, the implementation of the disk-backed container, and the user of that container often all will be different teams at different organizations) So, why declare that typed exception, in the first place?
- 9dev 2y agoBut isn’t the actual advantage here that it gives me the ability to handle that error case specifically? I mean stuff like IO errors can happen everywhere at any time anyway, but if I need to do something specifically if there is no such element, isn’t that a plus? Otherwise, I’d have no chance to handle that error if I can do so.
- MBCook 2y agoHandling there is probably the right thing to do. I think the argument is that there is probably a better way of expressing that in code than exceptions. Perhaps returning an Either<Data, ErrorCode> kind of value. There’s also an argument that that’s not truly an exception because it should be expected that an I/O error is a reasonable possibility. Things like OutOfMemory or VirtualMachineError are more what they may be thinking exceptions should be saved for. Truly unexpected things.