I notice this especially with less experienced developers and remote calls - a lot of JS code I’ve reviewed in the past assumes the remote call will always work, yet Java code from the same developer will almost always correctly handle the situation, simply because the exception is explicitly required to be handled.
They don't just "encourage" developers to consider error paths, they "force" them to do so.
The concept of checked exceptions is very sound, as is the more general concept of compiler enforced error checking. Very, very few languages have that (only Java and Kotlin in the mainstream league).
Languages with a solid implementation of algebraic data types offer a good first step in that direction but they still require users to manually bubble and compose monadic values, which introduces an unnecessary, and sometimes intractable, level of obfuscation and boiler plate.
All other languages provide weaker approaches to this concept that are library enforced, not language enforced, and therefore more prone to being overlooked since they require discipline from the developer.
For instance, consider fold or map functions. There was no way of saying "this might throw anything that might throw", so the only option was "this won't throw anything and that cannot throw anything" or "this might throw anything".
Without the necessary flexibility, developers aren't "forced" to consider only error paths but also manifold impossible paths that aren't easily distinguished from legitimate error paths.
1. You are not forced to use the result, so if you call a function for its side effects, the compiler will not warn you that you should unwrap the value. Rust and Swift do not have this issue.
2. This is extremely tedious, as you mention. But it's specific to Kotlin and not generally true as you later suggest. Haskell had monad comprehension (do notation), Scala has the same thing, Rust has the ? operator.
Also, regarding Kotlin; no library, nor the standard library, does anything other than throw unchecked exceptions on all kinds of failures. Kotlin is a step backwards, IMO.
Also PHP has checked exceptions, like Java.
try {} catch (Exception e) {
throw new RuntimeException(e);
}
Also not doing nothing, or the famous catch and log __attribute__ ((warn_unused_result))
in C/C++ with GCC and Clang is great.It's a noble goal, but once I started thinking about what happens in the failure case, I came to the conclusion that checked exceptions are no help here:
- there are always unchecked exceptions. I found it useful to think that any function might throw. So if extra reporting or graceful shutdown are required, just catch everything
- in most cases I have no idea how to recover from error: just keep throwing it to the caller until someone knows what to do. I want it to be the default behavior and I don't want to clutter my code with all the catch-wrap-rethrow boilerplate.
What's your alternative? Using return values? But then you are doing the bubbling, manually.
Exceptions offer a more elegant and less boiler plate approach to this problem: if your code can't handle an exception, just declare it in your signature and ignore it. This is really the best approach to this problem:
- The error cases are part of the function's signature (as they should). - The language takes care of bringing the exception to the right handler. - Your code can proceed with the assumption that all the values are sound.
Checked exceptions are part of your API, things that your code should be dealing with, and catch-wrap-rethrow is often the right thing. It only becomes a problem with Java programmer's tendency towards thin, shallow abstractions.
I’m not sure this is necessarily true.
There are always unchecked Throwable’s, but exceptions and Errors are quite distinct things.
Java's implementation has undeniable issues, e.g. in designing stuff with callbacks. It can and often does lead to "everything's runtime" or "everything throws Exception" or other hell-scapes people have heard of. Personally I still prefer them, for pretty much the same reason you mentioned - they are effective, especially with a bit of restraint.
Checked exceptions as a concept are not bound to that. And I wish more languages would make use of them. They can be just as flexible as ADTs, which are pretty widely approved of... because they describe exactly the same thing, just short-circuiting rather than requiring an explicit return. On that front, it's "exceptions" vs "returns" and there are plenty of opinions and tradeoffs between them.
Wait, if the example involves a remote system how can the Exception be the bottleneck? Even generating the completely optional stacktrace shouldn't take that much time.
I agree and like them in theory, but in practice the only practical thing that can be done with an exception is to get away from this section of code as quickly as possible and get back up to a layer where the user/system can be notified of the failure in some way. Cases where you actually can gracefully handle an exception like a missing file tend to be something you should check and not rely on exceptions for anyway.
My preferred style of error-handling is Option/Either, since I can implement the 'happy path' in small, pure pieces; plug them together with 'flatMap', etc.; then do error handling at the top with a 'fold' or 'match'.
Exceptions break this approach; but it's easy to wrap problematic calls in 'Try' (where 'Try[T]' is equivalent to 'Either[Throwable, T]').
The problem is that Scala doesn't tell me when this is needed; it has to be gleaned from the documentation, reading the library source (if available), etc.
I get that a RuntimeException could happen at any point; but to me the benefit of checked exceptions isn't to say "here's what you need to recover from", it's to say "these are very real possibilities you need to be aware of". In other words checked exceptions have the spirit of 'Either[Err, T]', but lack the polymorphism needed to make useful, generic plumbing. The article actually points this out, complaining that checked exceptions have to be handled/declared through 'all intervening code'; the same can actually be said of 'Option', or 'Either', or 'Try', etc., but the difference is that their 'intervening code' is usually calculated by the higher-order functions provided by Functor, Applicative, Monad, Traverse, etc.
It's similar to many developer's first experience of Option/Maybe: manually unwrapping them, processing the contents, wrapping up the result, then doing the same for the next step, and so on. It takes a while to grok that we can just map/flatMap each of our steps on to the last (or use 'for/yield', do-notation, etc. if available). It would be nice to have a similar degree of polymorphism for checked exceptions. Until then, I'd still rather have them checked (so I can convert them to a 'Try'), rather than getting no assistance from the compiler at all!
I'm not sure if it really addresses the underlying concern that the article presents though, which seems more like checked exceptions seem to be used in a way where the developer has no recourse anyways, so surfacing it through a monad or checked exception doesn't matter.
Yes, and I tend to write my code this way (although I find right-biased 'Either' a bit cleaner). The problem is (a) the mountain of JVM code which uses checked exceptions instead plus (b) the Scala compiler completely ignoring that exception information. Solving (a) is unrealistic, but (b) is an entirely self-imposed decision by the Scala developers. Their type checker could have incorporated checked exceptions in exactly the way you describe, and I wouldn't have any complaints (about exceptions, at least... null is a whole different beast ;) )
These functions wouldn't even need to care about Some[T]; T is enough.
| Polymorphic exceptions | Monomorphic exceptions
---------------------+------------------------+------------------------
Checked exceptions | Unsupported | Java
---------------------+-------------------------------------------------
Unchecked exceptions | Scala
If exceptions are unchecked then there is no difference between "inner" code and "outermost" code; the compiler cannot tell us that a handler is needed/missing. This is the case in Scala. The advantage is that we can compose code which throws and which doesn't throw (this is essentially dynamic typing for exceptions).If exceptions are checked then there is a difference between "inner" code and "outermost" code: the inner code has 'throws Foo' annotations, the "outermost" code doesn't. The compiler will spot missing handlers (i.e. when our outermost code can throw). There are two ways this could be done:
If checked exceptions aren't polymorphic then we need to make multiple versions of higher-order functions, like List::map: one version which doesn't throw, one which can throw one exception, one which can throw two exceptions, etc. (these exceptions can be kept generic, but the number of them must be explicit). For example if we have a lambda which can throw KeyNotFound we can't use it with the standard List::map method, since that only accepts lambdas which don't throw. We could make an alternative method 'public List<B> mapE(FunctionE<A, B, E> f) throws E', but that wouldn't work for lambdas which can throw FileNotFound and PermissionDenied; we could write a 'public List<B> mapEE(FunctionEE<A, B, E1, E2> f) throws E1, E2', but that wouldn't work for three exceptions, and so on. AFAIK this is the current situation in Java.
If checked exceptions could be polymorphic, similar to row polymorphism ( https://en.wikipedia.org/wiki/Row_polymorphism ) or algebraic effect systems ( http://lambda-the-ultimate.org/taxonomy/term/35 ), then we would have the best of both worlds. In this setup the 'E' in an annotation like 'throws E' doesn't stand for a name, but for a set of names. Higher-order functions like 'map' can throw the same set of exceptions as the lambda they're given, and that set could have any size: if the lambda can't throw any exceptions then map's set of exceptions is empty; if it can throw five types of exception then map's set of exceptions contains those five; and so on. This is becomes even clearer for things like function composition:
public Function<A, C> compose(Function<B, C> f, Function<A, B> g)
If 'f' can throw something from set E1 and 'g' can throw something from set E2, then 'compose(f, g)' can throw something from set E1∪E2. Likewise if something can throw E1 and we have handlers for E2, then the result can throw E1 \ E2.AFAIK the JVM can't do this, nor can those languages which typically target it (Java, Scala, Kotlin, etc.; Idris has algebraic effects and it can run on the JVM, although it's not the standard target)
Checked exceptions have bimodal usage, from the programmer POV. Either you care about the exception, and you deal with it very close to the throw point, or you don't care about the exception, and it should be handled far far away, across many stack frames.
The former isn't a problem. It's the right thing if e.g. you try to open a file and the file is missing, and you have reasonable error handling logic to retry, open a different file, replace the file and try again, whatever.
The latter is where the issue lies. If you're handling errors far away, then you're handling lots of different errors there, and you're not distinguishing between them, because there's too many different failure modes. You're most likely just in a loop logging errors, or terminating. You're too far from the cause of the exception to do anything specific with it, the context is lost. So the effort to transport the exception type throughout the call graph is pointless.
tl;dr: checked exceptions are fine near the leaf of the call graph, but are increasingly pointless towards the trunk.
At least, Either/Option force your code to take errors into account.
You still have to bubble them up and compose them manually, though, which is why checked exceptions shine.
Which is perfectly fine. If you _can_ handle the exception near where it happened, do so. Otherwise bubble it up and log it or whatever.
Consider a REST service. If a DB or other error happens, I can retry right then and there if business logic calls for it (probably not), or just kick the can down the road where something appropriate like a 500 response serializer will take care of it. That’s a great pattern in my opinion.
I get what you're saying, and things like 'Try[T]' lose the specifics of the error type too (we just get a 'Failure(Throwable)').
My problem occurs earlier on: which code might throw exceptions, and why? Java's checked exceptions let methods declare "I can fail with an AccessDenied error (along with all the usual stuff like NullPointerException, etc.)", and the compiler will make sure that's handled somewhere (even if it's just a log and quit).
Unchecked exceptions are implicit; the signatures don't tell us they exist, and the compiler doesn't check that they're handled. This makes sense as a last resort, for things like OutOfMemory (although Zig would disagree!); but in general it's unhelpful and dangerous. I think it's a poor choice on Scala's behalf to treat all exceptions this way. Their only redeeming feature is short-term convenience; it's essentially an instance of static versus dynamic typing. "Exception polymorphism" (which I imagine would look something like row polymorphism) would make checked exceptions more convenient, since we wouldn't need exception-specific boilerplate, and this might be enough to solve the problem.
Maybe the JVM might gain such a feature in the future, but until then we can use 'Either' or 'Try' to achieve a similar thing: they show us explicitly which methods can fail (answering the question in my second paragraph), and they force us (via the type checker) to handle the error case somewhere (even if that's just a generic log+quit handler at the top level, as you say).
I'll write my library code with checked exceptions. You call my library code in one of your methods. The compiler tells you to do something about the possible failure. If you want to handle it there, you handle it with a try{}catch{}. If you don't, you wrap it in a RuntimeException and rethrow it so your top level handler can deal with it.
Perfect.
Unchecked exceptions make it easy to fuck up the case where you actually might want to handle it close to the call.
Rust developers can fluently switch and convert between conrete `Result<T, SomeErrorEnumeration>` and `Result<T, dyn Error>`. It works beautifully. Libraries usually enumerate their errors (leafs), applications usually just throw everything to one universal error bag.
Here's what Oracle has to say:
> Here's the bottom line guideline: If a client can reasonably be expected to recover from an exception, make it a checked exception. If a client cannot do anything to recover from the exception, make it an unchecked exception.
https://docs.oracle.com/javase/tutorial/essential/exceptions...
- checked exception for recoverable errors
- unchecked exception for non-recoverable errors
So the argument that most errors can't be recovered from is _not_ a reason to abandon checked exceptions. It's a reason to reserve checked exceptions for those cases in which recovery is likely.
The main argument in this article appears to be based on a misunderstanding.
Even though I completely agree that checked exceptions make Java a huge pain to work with, I do feel something has been lost when I use languages that don't support them or any alternatives. Having no way to enforce systematic handling of an error isn't a great solution either.
That's the thing though, right? If you're writing a program with a UI you want to pop up a box saying "File not found, choose another".
A server program might format an appropriate response, log it and send it back to the client.
A batch process may just want to bail with a code.
So it would seem to make sense to have it checked...
Trying to make those decisions as a library author is a sort of combination of the tail wagging the dog and self-fulfilling prophecies.
Depending on the client's ethos and operating parameters, they may or may not reasonably want to try to recover from anything. Java's designers decided that failing to reserve memory for a new heap allocation isn't something that people might want to recover from, and so it really is impractical to do so - for lack of documentation as much as for any technical reason. On the other hand, Zig decided that developers should be able to recover from it, and so it's really not such a big deal for Zig developers.
Personal example: I tend to be a "let it crash" person, so I generally write code that throws exceptions that really are hard to handle. Not because of any fundamental reason, just because that attitude leads me to be a bit more loosey goosey about organizing and documenting the exceptions that I throw. I get away with it because it's all internal projects. If I were writing an open source library meant for public consumption, I'd probably try to be more disciplined, so that users could choose their own moral paradigm.
They're overly ceremonious for relatively limited functionality (don't encompass the broad range of circumstances where you would want to have multiple return types) and poor composability (e.g. lamdas).
As unfortunate as that is though, I don't really think it's something that can be done better. Ultimately errors are breaks in a function's abstraction. There's nothing that a readFile() function can do about a missing file, because it doesn't know how that file was selected or what you're going to use it for. The only way around the error would be absurdly coarse abstractions like readUserSelectedFileInGuiProgram() and readCriticalFilePackedWithProgram().
Better syntax can reduce the boilerplate - algebraic types plus match expressions are certainly an improvement. But the guidelines for when you should return one end up the same as the guidelines for a checked exception, since they're expressions of the same idea.
Library and framework developers mostly only know that a client cannot recover from an exception; rarely can they make good judgement about when a client can recover from one but they have to bake that choice in.
It's more flexible to encode error or otherwise abnormal returns into the return type - there is basically no reason to prefer a checked exception.
doing the opposite of what Oracle has to say seems a good guideline in life
That's a philosophical position that is largely driven by the language (and I suppose the ecosystem around it). I happen to agree with that position, so I prefer languages like rust and go over java.
But I also know that if I try to fight the customs of the language I'm working in, it'll end in a lot of pain and unnecessary angst; so, if I find myself using Java, I grit my teeth and used checked exceptions.
So now we have consultingware like Spring where if something isn't working, it could because you missed an annotation somewhere, or put the right annotation in the wrong place. Which annotation? Where? Maybe you'll find out a week from now that you made a mistake, when a customer finds a bug in production.
This took all of the compile-time checking goodness that you got from Java and threw it in the garbage. Now you either have to call an expensive consultancy, read books/manuals about your gigantic framework (fun!), go on forums, etc. You can't just use your coding skills.
I still often use Java for my side projects because I love it without runtime annotations, but thank god for the rise of Golang. I'd rather deliver pizza than go back to the misery that is annotation-driven development in Java.
It's quite possible to avoid them of course and I prefer just to use code to instantiate objects rather than learning yet another configuration language attached to fields.
Now add to that the fact that if the runtime can't load the annotation class via the classloader, it just silently pretends the annotation isn't there.
The problem with inversion of control is that if you get it wrong, there is no feedback. All you get is "nothing happened". When it works, it just works, which is great. When it doesn't work, it doesn't work, and "it doesn't work" is practically impossible to Google. Spring had the same problems, worse, with XML configuration.
The solution is exactly as you say: let programmers program. Solve the problem with debugging tools that you already know, rather than introducing a whole new meta-meta-programming environment without any debugging support. (If you attach a Java debugger to a running Spring program to step through the point where it's failing to find your annotation, you will regret it.)
https://github.com/dotnet/roslyn/blob/master/docs/features/s...
Why not just use code (generated if required) to read xml or json for each class? That way it is clear, in the source code and can perform other transformations as required.
I see the problem.
The whole point of spring XML was so you didn't have to "write code" to wire things up. Now we're using annotations to replace XML - we're writing code so we don't have to write code. It makes no fucking sense.
Annotation injections are a completely ridiculous turn of events.
What I consider a mistake is not making override a proper keyword.
Interesting. Could you elaborate on why you find that so?
By way of illustration regarding why I ask: I've been writing Java code for 20+ years now, and I can't remember a single time that the "default virtual" behavior wasn't what I wanted. And I wrote C++ before moving to Java so I'm familiar with the approach of requiring one to mark methods as virtual explicitly. I've just always found that the Java approach makes sense. What am I missing?
The reasoning goes stepwise.
1. In a good design, a class interface represents an abstraction. The public interface provides access at the level of the abstraction, obscuring details of implementation.
2. If it is a good abstraction, by definition it maps the public view to an internal model that differs from the public view.
3. Inheritors provide different implementations of the abstraction by defining their own versions of inherited virtual-function signatures, operating on the internal, abstracted model.
4. Because these virtual functions operate on the implementation, they should not be exposed to the client. Exposing them would violate the abstraction, exposing the internal model.
5. The public members of the base class translate abstract operations to operations on the internal model, and call virtual functions that provide variable concrete operations on it.
6. Therefore, it is wrong for the public members of the base class to be virtual: their interface is the public view of the abstraction. It is their job to map that view to the internal model, and to call the virtual functions that implement the model.
7. Similarly, it is wrong for the virtual functions to be public, because they implement the internal model. Making them public would expose it.
All that said, since Java offers only one organizing mechanism, the class, it necessarily gets used for everything, and not just for well-designed abstractions. It is not wrong to treat language features as a pot of mechanisms, and use them in ways that do not match their designed intent. Coding Java, there is no choice about it; classes are all you have, so you use them for everything, and the well-designed abstraction may be a rarity among all the other uses, and might not exist at all in many programs.
But Java's limited feature set is out of scope for discussion of the designed purpose of virtual functions. Used according to the Object Oriented model, public virtual functions are an oxymoron. But as mechanism, they are as usable as any other, if they work.
It would be a huge mistake in a statically compiled language because the static compiler doesn't have as much information as a JIT in flight.
There are two different ways to create a subclass in OOP. Java treats the subclass and the superclass as the same object. If the subclass calls a method on itself, it may go to the superclass first. This is a valid way to do things, but it can be dangerous and confusing.
Another way to create a subclass is to create two objects: the superclass and the subclass. The subclass has a reference to the superclass, but they are not the same object. If the subclass doesn't override one of the superclass's methods, then it implicitly proxies the method call to the superclass. If the superclass calls a method on itself, then it goes to itself rather than the subclass.
The difference between the two approaches is whether you want your class implementation to be open to extension (monkey-patching). Allowing class inheritance (Java's approach with the virtual methods) means that subclasses can override methods you define. It can be convenient, but it can also be a foot-gun. Class composition (proxies) prevents monkey-patching by subclasses.
Java makes a lot of common classes final (e.g. String and Optional) so that it can avoid this. As I understand it, it's also got a lot of pixie dust to predict this some of the time, but then you can't rely on it.
One downside is that every method now requires following a pointer, which is pretty awful for performance on modern hardware.
This seems like some C++ pattern holdover where you obviously do have virtual vs non-virtual methods. 99% of the time I suspect it's just a source of extra bugs and the vtable lookup really isn't the performance hit (most of the time) you think it is.
Or maybe I just want to defer the error management to a higher level. Again busywork declaring exceptions.
try { //your code } catch (Exception e) { throw new RuntimeException(e); }
around it and you'll be fine.
The thing about Java is that the code lasts for a really long time because when it fails, it does so usually with an exception that points to the problem. When promises hang in NodeJS or memory corruption happens in C, it's much more annoying to track down problems.
I mean, why are you writing a kleenex program in Java? Do you not want to declare a return type for your functions either? Java's checked exceptions are just a way to return multiple types from your methods: a success value and a failure values.
Use Groovy. Seriously, you can code it up with zero boilerplate, use the REPL, notebooks, etc to get your idea into shape, all with 100% interop with your Java libraries.
When you are done, if parts of it should graduate to "real code", you can pretty easily port it over or keep parts of it in Groovy and just take the elements over that need to be Java for robustness, etc.
This is actually my standard workflow in development now.
I'd also disagree that adding a throws to the signature is much boilerplate at all. Especially in the ages of IDE's
while (iterator.hasNext())
System.out.println(iterator.next());
Take away unchecked exceptions and iterator.next()'s NoSuchElementException either becomes checked (requiring handling code that's unreachable) or gets removed (hope no-one ever forgets to check hasNext)This is one of those things that should have never been done with exceptions, I'm looking at you Python, but with Maybe. Then you can either handle the None case or prove to the compiler that it's impossible.
`next()` should return an option that you can `map` over.
Also checked exception haters always overlook the fact that CLU, Modula-3 and C++ did it first.
Checked exceptions are pretty much isomorphic to result-types that are oh so fashionable these days (see also Rust), unchecked exceptions on the other hand are completely invisible and unpredictable crazyness.
Rust improves on this by using a different syntax for unchecked exceptions (`panic!` vs `Result`) which provides the benefit of discouraging unchecked without preventing it.
The modern noexcept specifier is closer to checked exceptions, but has learned some of Java's mistakes (for example, a function template can be conditionally noexcept based on input arguments).
And they were the inspiration for Java's.
Even with all the functional stuff they almost never seem to really create a major issue.
My biggest issue with Java is just the way they've caved and constantly added new stuff that is always grafted on so it's never quite as good as a language that focuses on that programming paradigm from the start.
But none of the problems in the language compare to the scale & scope of the problems caused by Java's default developer & architect culture. The culture is terrible... everything gets overcomplicated, overabstracted, etc. and you've got charismatic charlatans convincing wide swaths of developers to misuse and abuse language features in ways that have made a lot of people hate the language and have produced a lot of buggy and hyper inefficient code.
Java itself doesn't have to be bloated, slow, buggy, and a massive memory hog. But the java developer community has continually made decisions to structure their java software in a way that makes that the default condition of Java systems. The way the Java language constantly gets new giant features grafted on plays into this.. everyone jumps on the latest language addition and misuses it for a few years before they come to understand it. By the time it's understood there's something new to move onto and abuse.
Java became everything about C++ it was originally supposed to simplify.
In order to tests classes in isolation like this, all dependencies must be injected, so they can be stubbed or mocked.
Because there's no universally available IoC framework or baked-in functionality, everyone takes the library approach to DI, which means factories of various levels of abstraction, and an excess of parameters.
But in practice most production Java systems do run with an IoC container, which in turn makes more fun things possible - AOP, interceptors, automatic transactions, all manner of magic.
(There's no doubt that checked exceptions were a mistake, to my mind, but the mistake is being repeated in Rust, so I guess that lesson hasn't been learned. But perhaps all checked exceptions needs is a better type system to manipulate the error types through the control flow graph. We'll see.)
This really boils down to which school of testing you follow. London (Mockist) vs Detroit/Chicago (Classicist). [0] is a good article on the different views of testing. Made me think more about it.
I find that using fewer mocks, and unit tests that integrate many classes (vs one set of tests for each class) makes Java programming more fun- but it is very much a Detroit school way. And that's okay, but it's good to really think about the differences.
[0]https://medium.com/@adrianbooth/test-driven-development-wars...
Magic stuff like this makes maintenance a nightmare. All these random attributes turn regular code into a 4d jigsaw puzzle - the complexity OP is talking about.
Stop talking about the Java community like it's a monolithic thing. It's far too big for that.
Isn’t that basically the #1 issue with C++? They needed more and more features to compete with newer languages and, in the end, the language feels like a Swiss Army Knife. It’s got a tool for every situation but it’s ultimately impossible to grab and use with all those tools making getting a grip impossible.
C++ is pretty clear on the right way to do things at a given point in time. You will run into a similar problem when working with old code, you need to decide whether to abandon the new features to stay consistent, rewrite your program to make it consistent and new or do something in between and be inconsistent.
The problem with C++ is that the prevalence of macros and #include make backwards compatibility effectively impossible to patch around.
So you end up having to support ancient C++ code working as it was written decades ago working exactly the same. You can't even use file level flags because the object file in almost every case is going to #include old code.
The compiler can't stop you from doing things the wrong way due to this. You could have other tooling that warns you that your code is making X mistake but the compiler can't enforce that or assume that you don't make that mistake due to this extreme backwards compatibility requirement.
Side note: backwards compatibility in C++ is great and fundamental as it prevents fragmentation of the language between different incompatible dialects. The requirement alone doesn't fundamentally cause C++'s problems it just eliminates the easiest way to solve them. (Assuming Python 3 was easy)
Nevertheless, I still believe that Java's biggest mistake is not checked exceptions, but the stupid distinction between primitive types and Objects (caused because all the cruft present in the latter would have make basic operations prohibitive in terms of performance, at least for early Java versions) and all the associated boxing. I have seen some extreme cases of performance degradation because of that (fortunately a refactor to use arrays solved the problem, but this is not always possible).
This irks me too. It's been a while but I believe the workaround is just to define your own (checked) FunctionE, SupplierE, ConsumerE functional interfaces. But maybe that causes other problems I've forgotten.
> the stupid distinction between primitive types and Objects
I also dislike the distinction between primitives and Objects. But I don't think your argument follows. Performance is why the distinction exists. Objects have the cruft and primitives don't.
This distinction wasn't such a big deal in 1995. It was perhaps even the preferred way of doing things according to contemporary values. Nowadays, though, Java is a very different language that lives in a very different cultural milieu. So, nowadays, the language-level distinction, in combination with some other design decisions that came later, is absolutely a source of performance problems. Any code that tries to do things like store numbers in one of the standard collection classes will quickly devolve into a horrible mess of memory overhead and pointer chasing. It's led to this perverse situation where it's honestly not too hard to beat Java performance in a dynamically typed language like Clojure or Python, simply because dynamic languages make it easy to encapsulate the implementation details (and therefore to choose a more performant run-time implementation) in a way that Java's type system doesn't really allow.
You can still keep Java toward the top of the well-known performance benchmark leaderboards, but only by coding as if Java 5 never happened. Which is an option that's only tenable for toy use cases like benchmarks.
You can do that, but you need to do it for every exception type ): I'd love it if Java had templated exceptions.
>> the stupid distinction between primitive types and Objects
One of the slowest things I found out about C# was floats being objects and `a < b` (or something similar) being a stack about 7 levels deep.
There are third party libraries that alleviate this. I use koloboke a lot, for example (which works for maps and sets, but lacks sorted collections and multimaps, which I also need to use very often); but the problem remains for anything slightly complex, and it's not uncommon to find yourself writing two almost identical copies of a relatively complicated method or class, one for objects and another one for ints (and maybe a third one for longs, a fourth one for floats...), because otherwise you hit the same problem.
So yeah. Primitive types are not objects and that limits them because Java doesn't work well with things that are not objects. C++ is much better in this sense, because generic code is actually generic and any type will do.
For example, it took forever to be able to handle two unrelated exceptions with the same catch block, and incredibly common things like closing file handles would require nested try / catch blocks in all the primary catch clauses (and now every utility library ends up with "closeQuietly" or similar ...).
I think if Java had done it much better there would be a massively different opinion of CE.
But then there are other types of checked exceptions that are almost certainly unrecoverable because they happen way down in some other third party code. And then you get the endless chain of "throws" all the way back up the code base.
No you don't. When your code calls ThirdPartyAPI and it throws one of these checked exceptions that you know you can't recover from, you wrap it in a RuntimeException and rethrow. Then it's totally invisible to the rest of your code.
Likewise, you should never have a method that throws 10 kidns of checked exceptions. You should be writing your own custom exceptions that wrap downstream exceptions into forms that are useful for you and your code (or people who will use your code).
The biggest issue with checked exceptions is that people refuse to think through their unhappy paths.
Take java.io.Reader - it represents an arbitrary input source, so the Reader.read() method is declared to throw the very generic IOException. The subclasses don't make these any more concrete, leading to absurdities like StringReader.read() having a checked IOException.
Actually, no. Sometimes it is fatal, and sometimes it's not.
If the file being open is expected to be part of the distribution and your app can't start without it, it's fatal.
If it's a file picked by the user, it's most likely recoverable.
The main problem is that every single one of these exceptions should be able to be either runtime or checked, and that choice should be made by the application.
Open question: should this decision be made at the call site or the use site?
My position is that the caller decides how to deal with exceptions. When I'm adding a new operation to my web app, I won't add any exception handling at all. Why would I? There's a central exception handler in my app, which will log exception, analyze its type and return an appropriate status code to the caller (e.g. 500 or 400). If there's a kind of failure my centralized handler doesn't handle correctly, I'll add an explicit handler in the new operation, which would do the right thing in some cases, while the rest of issues are handled centrally still.
Checked exceptions make me have dummy handlers all over the place.
I have a throwable type JVMNotSupportedError[0] specifically to wrap these possible but will never be thrown exceptions. Its sole reason for existence, theoretically, is to yell at the user to get a better JVM.
[0] https://github.com/theandrewbailey/toilet/blob/master/libWeb...
[1] https://www.joelonsoftware.com/2001/04/21/dont-let-architect...
- database connection closed
- SQLTimeoutException (o.O)
- protocol exceptions like duplicated key
- query exceptions
the problem is that some of them are recoverable but usually it's more of an hassle and duplicated key exceptions are easier to handle in app code via upsert, etc.so basically the problems are library designer errors and not application writer errors.
checked excpetions should only be exceptions that a developer COULD handle and not ones that he can't
in C# the library designers usually create a "Result" object that has a boolean of succeded or status of enum instead of using exceptions for failures. i.e. most i/o errors are not recoverable, thus c# does not enforce you to recover from them.
Here's a bigger problem: checked exceptions pollute your API and expose implementation details. Consider an API that stores and retrieves objects. A particular implementation does so by writing them to a database via JDBC so you get SQLExceptions. You have two basic approaches:
1. Include SQLException in your function signatures so the caller can deal with it. This exposes the implementation detail; or
2. You can hide it by transforming that checked exception into something specified for your API. At this point, what benefit have you gained from SQLException being a checked exception? You're hiding that detail.
For (1), you're baking checked exceptions into your function signatures such that it can be really difficult to change later on.
If you sit down and think about the practicalities the argued upsides of checked exceptions are essentially nonexistent and unchecked exceptions are actually strictly superior.
Here's another pattern that happens with checked exceptions in Java:
try {
doSomeSQL();
} catch (SQLException e) {
// do nothing
}
You will see people do this all the time because they don't want to deal with the checked exceptions. A better catch-clause is: throw new RuntimeException(e);
You can argue people shouldn't do the first and they shouldn't but unchecked exceptions will simply bubble up unless you deliberately swallow it. That's a way better default. Defaults matter.And that's kinda the problem; you can declare the ones a user MIGHT be able to recover from, but there's still the chance of unrecoverable or unforseen errors, so you wind up declaring THROWS EXCEPTION anyway...
> in C# the library designers usually create a "Result" object that has a boolean of succeded or status of enum instead of using exceptions for failures. i.e. most i/o errors are not recoverable, thus c# does not enforce you to recover from them.
Depends on the operation but yes. Either There's a Try___ Pattern (where boolean is result, and 'out' parameter from method is your parsed value) or some will do an enumeration pattern.
Still, I'm a fan of Option for these sorts of things nowadays...
> - query exceptions
Even that is just barely scratching the surface. Postgres has ~250 error codes, and while not all of them can be triggered by all operations or statements, there's way more granularity in there than there is in just two piddly exceptions.
It would be great if they worked the other way around. Instead of forcing the caller to catch an exception, they would guarantee that no exception leaves a certain block.
So you would have a function
void MyFunc() onlythrows IOException {
first();
second();
}
And the compiler would statically guarantee that no other exception can leak out of it - because first and second have been marked `onlythrows IOException` or are "pure" and cannot throw at all.For sure you'd need an escape hatch, like Rust's "unsafe". And it would not be very useful around legacy libraries. But it would be tremendously useful if you could drop a block like
neverthrows NullPointerError { ... }
in your code and be sure that everything inside is null safe! I asked about this a few years ago on StackExchange [1] but so far I never heard about it anywhere else.[1] https://softwareengineering.stackexchange.com/questions/3497...
I'm not sure I I'm reading your comment right, but this is plainly false. Caller can add a throws declaration themselves and catch anything.
It seems to me that what you're advocating bears no difference at all with checked exceptions. The "unsafe" escape hatch is called RuntimeException. "throws" behaves exactly like your advocated "onlythrows".
The only actual difference you're proposing, AFAICS, is that you'd like java to handle nulls differently - and I think we can all agree on that.
You are right. I meant someone in the call stack has to catch, not the immediate caller.
That I mentioned null pointers is a red herring. I want to add a block that tells the compiler "prove that no exceptions (checked or unchecked, not even NPE)" can escape outside of this block!".
The escape hatch is about this: imagine you have a function that throws if the argument is odd, but you know you will only call it with an even number:
void myFunc(int i) never_throws {
swear_never_throws(NumberOddException) {
throwsIfOdd(i*2);
}
}
(Apologies for the pseudo-code, I haven't made up a nice syntax.) If you mark your function that no exception can escape (not even an unchecked exception!), but the compiler sees that throwsIfOdd can throw NumberOddException, you must of course assert that what you are doing is OK.Your "neverthrows" looks to me just like a different way of expressing try/catch
A "neverthrows" block would not compile if there is anything inside that can throw (even a runtime exception). Its a way of drawing a line. A library author could use it to make sure no unexpected exceptions can bubble up.
And yeah, if you get NPEs at random you are making a mistake. If I could just choose to not make mistakes, I would :-D. But until then, I'd prefer the compiler to check my work. (And catching them is not an option. What do you do then? Terminate the program? Ignore them? They need to be found at compile time, not at runtime.)
It conflates thread management with exception handling in a way that's difficult to understand and implement correctly. The relationship between InterruptedException and the Thread.isInterrupted() method is a particular pain point for coders.
Something like Erlang, where any process will just die upon being sent the exit message, whether it's blocked on IO or receive or busy in the CPU, would definitely be simpler and easier to reason about. But that capability carries a runtime cost.
Go's mechanism is also arguably simpler, in which goroutines are not first class objects and if you want to be able to interrupt one then you have to write ad hoc logic using a channel that you provide specifically for the purpose. But in 99.9% of cases, I think Java's more complicated mechanism with first class threads is more convenient.
I would make it an unchecked exception, though. And I wish the old java.io.Socket operations and similar methods would throw InterruptedException.
Because that's the other thing--you can't guarantee InterruptedException will even be delivered to a thread. An underlying library can just eat it or the thread could be waiting on a socket [1], etc. This kind of behavior the bane of correctness or even getting operations like clean server shutdown to work at all in some cases.
So I think this really needs to be something like CSP that's built into the language in a way that makes the behavior consistent in all cases even if it introduces coding patterns that have other costs. Java _did_ get object locking and data visibility right by building in simple primitives like the synchronized keyword into the language. You can create deadlocks but the behavior is clean enough it's not hard to program around them.
I would also be fine with just dying as long as there is a way to clean up shared data structures. However, that can't be manual because it's just about impossible to ensure that such cleanups are correct. Databases use transactions to get around that problem.
[1] https://stackoverflow.com/questions/1024482/stop-interrupt-t...
String s = null;
switch (s) {
default:
System.out.println("Hey");
}
Hint: it will throw NullPointerException. switch(s) {
case null: return false;
case "y": return true;
default: return false;
}
The broader issue is that Java handles null inconveniently.1) Every object can be null, switch requires the argument to be non-null, and the type system doesn't warn you when NPE are possible. A type system which handles nullability could fail to compile if 's' is nullable. Kotlin does this, and it let's you opt-in to the NPE with some convenient syntax:
switch(s!!)
2) It's inconvenient to handle null as a value. To properly handle the null case without throwing, you need to do one the following: if(s == null) { ... }
else switch(s) { ... }
switch(s == null ? "some-default" : s)
The first way can be made more convenient if you change switch to work on nullable values. The second way is inconvenient, so people generally skip it. If you want switch to only work on non-null values, there're more convenient syntaxes to handle null, such as the 'elvis operator': switch(s ?: "some-default")For me type erasure is a bigger issue. I get that it was done for backwards compatibility but the drawbacks imposed by that decision seem to only grow as more time passes and more new compromises have to be made.
I’m sure a bunch of material would have been written describing how to avoid runtime code duplication by tweaking your class hierarchies.
- List<T> doesn't "just work" with non-reference types. It needs boxing that increases memory usage and introduces stuff like ints being null.
- We need special functional interfaces for non-reference types for that reason (e.g. IntConsumer).
- This also affects Stream<T>, so we need IntStream etc.
- A method must have a parameter of that generic type (or it has to belong to a class that has this generic type). It otherwise becomes indistinguishable during rumtime. For example, a method like ImmutableList.CreateBuilder<T>() is not possible in Java (that example is from C#'s collection types).
Type erasure moslty comes into play when looking at non-reference types. For reference types, it seems to work pretty good (although it's weird that Map<String, String> will have the same runtime type as Map<Object, Object>). The last point I mentioned is not good, but no deal breaker. If generics would be like in .NET, we wouldn't have any of these restrictions.
With type erasure, we ironically have to write more java code while not being able to express stuff in an abstract manner (Stream<T> is incompatible with IntStream).
I've never understood the angst. All the arguments reduce down to mitigating terrible abstractions.
The Correct Answer is better APIs. Mostly, that means don't pretend the network fallacies don't exist. Embrace them. Which generally means work closer to the metal.
I'll say it another way. The problem is EJB, ORMs, Spring, etc. The obfuscation layers.
Someone smarter than me will have to rebut the functional programming points. I'd just use a proper FP language. Multiparadigm programming is a strong second on the list of stuff you shouldn't do. (Metaprogramming is first.)
Any API which has used checked exceptions was made worse by those, because Java's checked exceptions are bad, and their use runs actively against good APIs. So not having them would have made the corresponding APIs less bad (not necessarily good, mind) by definition.
Why would network programming (I/O, persistence, etc) look any different whether the language was C, Java, GoLang or other?
Most of my code is error checking and handling. (And now logging too, which I'll ignore here.) Is this abnormal? (Being rhetorical.)
Plenty of noob code ignores errors. I sort of thought we all decided that was suboptimal. Java's response was checked exceptions.
The only argument I've ever heard that made any sense is the silliness of catching exceptions so far removed from the root cause that your code can't do anything about it.
So don't do that.
Really, why would any one design a system that way? Because network programming is messy? Because it'd be neat to compartmentalize the messiness?
I've been able to cleanly separate the value add business logic from real world messiness exactly one time. I was in control of the full stack, end to end. Imagine something like a useful BizTalk. I had been inspired by postfix. My engine would pass work to plugins, which didn't have to do any I/O of their own. My work anticipated serverless and AWS Lambda, if those programming frameworks (paradigms) were better designed.
It now occurs to me that the checked exception abolitionists are advocating Happy Path Programming.
The only other feasible Happy Path Programming strategy I know of is Erlang. I've only done an Erlang tutorial, nothing in prod, so this is just a guess.
I quickly found that checked exceptions just do not play nice with any sort of functional-style programming, like the article describes. But the problem goes so much deeper than that. Checked exceptions are also, as far as I can tell, incompatible with an object-oriented programming style. More or less for the same reason that they interact poorly with FP. The fundamental problem is that checked exceptions don't really play nice with polymorphism or higher-order programming of any type.
Which takes us to the crux of how I understand them now: Checked exceptions may not go well with FP and OOP, but they make all the sense in the world if you're doing procedural programming. There, you're not trying to create deeply nested abstractions, and you're not messing around (much) with polymorphism tricks. The code's very lexically organized, with little in the way of dependency injection or higher-order programming. When you're programming procedurally, it's fine to be exposed to the implementation details of stuff further down on the call graph, because you're the one who put it there in the first place.
And that, in turn, means that checked exceptions are not really a mistake. They're just a piece of evolutionary history. Because, early on, Java wasn't really an object-oriented language. It was a deeply procedural language with some object-oriented features. It arguably still is, it's just that there's been a big cultural shift toward trying to take a more object-oriented approach since Java 5 came along and made it more practical to do so.
So yeah, it's true, Java has classes. But its culture and idioms and standard libraries and even some language features (checked exceptions, for example) are forever pushing developers toward procedural idioms. Less so now, perhaps, but intensely so in the 1990s.
However Rust, unlike Java, has a great macro system and can thus easily generate higher level exceptions wrapping the lower level ones.
That the language provides tools to operate on both results themselves and their content (in part because results are reified and thus normal values of the language, and in part because specific tooling like `?`) is what does that.
Also that there is no issue of misclassification as in Java, because everything is a result and that's that.
I don't hate Java's checked exceptions. But I also actually craft my own Exception types when I write a Java package. I think that's the biggest mistake that devs make. In Rust you have to combine errors into composite error types. In Java you should do that.
Checked exceptions have all the disadvantages of monads:
* Checked exceptions don't compose with each other. You have to create manual witnesses of composition (methods throwing multiple exception types, or wrapping exceptions into new exceptions like a monad transformer)
* Checked exceptions infect the type system, having to be copied everywhere.
However, they have none of the advantages of monads, and more disadvantages besides. Java the language does not provide any facilities for abstracting over checked exceptions, and they interact terribly with any design involving higher order functions or any other higher-level abstraction.
It's time for the java community to admit they got this one wrong and move on.
Can you explain what you mean by this?
public <T> higherOrderFunction(Function<T> f) throws whatever f throws { }
I can write a method that takes in a function object that throws zero checked exceptions. I can write a method that takes in a function that throws exactly one type of checked exception. I can write a method that takes in a function object that can throw two types of checked exceptions. And so on. But this involves lots of copy-pasting, which is the opposite of abstracting.
Checked exceptions are not first-class citizens in the language (see https://en.wikipedia.org/wiki/First-class_citizen). Unlike return values, I can't, for instance, in the general case assign the possible checked exception thrown by a method to a variable without losing type information. To do so would require sum types, a feature which most popular languages don't have.
On the other hand, if I create a class like IO<T>, which represents a possible return value of T or an IOException, then that is first class in the language and I can do anything with it that I can do with any other first-class value in the language.
catch (MyCheckedException e) {
e.printStackTrace();
}
This causes unreliable programs, since the programmer will initially only think about the successful path. Eventually, the exception will get thrown, and things will break in weird ways. They may not notice it quickly, because the stack trace will be buried in logs. Alternatively, if the IDE default has been throw new RuntimeException(e)
or something similar, which would crash the program, the programmer would have noticed it more easily. Of course, the program would still be broken, but better crash hard and violently than subtly and confusingly.Monads are highly cumbersome, unwieldy, and difficult to use, but Haskell programmers put up with them anyways because they get specific value from them, being able to say things like:
* effect tracking * continuations * tracking which methods perform I/O * tracking errors
Java checked exceptions are like monads, but worse: they are even more unwieldy and interact poorly with the rest of the language. And yet, unlike monads in a language like Haskell, they provide practically zero value for their cost.
There is a reason no language since Java has copied checked exceptions as a feature, including C# which started as a direct rip-off of Java. There are better ways to encode errors into a method signature to try to force the caller to consider them than checked exceptions.
FTFY
See long discussion here: https://forum.dlang.org/thread/hxhjcchsulqejwxywfbn@forum.dl...
I'll get one in a million chance of careless library developer randomly changing exception types over writing `throws` and `try/catch` statements a million times even when I don't need to handle any exceptions at all.
I once attended a Java user group meeting where James Gosling (Java's inventor) was the featured speaker. During the memorable Q&A session, someone asked him: "If you could do Java over again, what would you change?" "I'd leave out classes," he replied.
https://www.infoworld.com/article/2073649/why-extends-is-evi...
1. Built-in exceptions that are checked but should be unchecked, IOException being the main offender (I don't mean things like FileNotFoundException; I mean the kind you can get if the OS returns -EIO)
2. Lack of exception polymorphism, preventing you from doing things like l.stream().filter(SomeClass::somePredicateThatMayThrow), even if the function that you're doing it from can throw the same exception that the predicate can
I think checked exceptions would be great and nobody would hate them if those two problems were fixed.
We applied ROP with great success at a fintech where we wanted to clean up a block of business logic with many failure paths. Instead of a forest of nested conditionals or try/catch mess, there was a very simple happy path with clear handling for all the errors.
Here's a good start. Ignore the language details, the concept is universal. https://fsharpforfunandprofit.com/rop/
A big part of the reason people hate checked exceptions is that actually doing #1 correctly is really damn hard. It's a whole separate dimension of complexity that your design needs to tackle.
A compiler that checks exceptions forces you to do it. Abruptly, if you're new to the language. It flips the floodlights on at full brightness and makes you see the full scope of the problem. It's tempting to shoot the messenger.
Java's checked exceptions got the worst possible combination of error tracking. Its optional, so you don't even get to see if a function throws (kind of like the billion dollar null mistake) and its based on classes, which means a lot of irrelevant names leaking throughout the codebase.
Like with nulls, the main value is being able to claim that a function doesn't ever throw, at all. Apple's Swift got this just right.
A simpler system of "throws / doesn't throw" and with optionally polymorphic variants / unions to complement it would go much further.
A long time ago, I developed quite a lot of code in Ada83. Our team found having to use documentation to express what exceptions might be thrown led to many errors. I was pleased when Java came around that this was expressed directly in the function declaration.
But then it became clear that it lead to a lot of boilerplate.
I would like the throws keyword to just be an indication of what might be thrown and not require that I catch it.
You can use catch block style with typed exception handling or simple boolean checks depending on the situation and what you prefer.
Some checked exceptions, such as InterruptedException, should really be something else. I've very rarely seen this exception handled properly by anyone. Often, a general catch Exception will also catch this, and while the code will work just fine in most circumstances, random threads will not go away when they should.
It's a mess.
This isn't to say that checked exceptions are always used well. (What exactly am I supposed to do about an exception from .close()?)