Loading...
Loading...

Java's checked exceptions are both an integral part of the language and one of its most contested features. Whether their introduction was a mistake and whether they should all be turned unchecked are frequently discussed topics but since the former is not overly relevant and the latter unlikely, this conversation isn't moving Java forward. Instead, let's talk about specific issues with checked exceptions and what could be done about them - from (entirely speculative) language changes to (marginally realistic) JDK/library evolution to stylistic changes.
Hello and welcome back to the Inside Java Podcast, the podcast about everything Java brought
to you by the team at Oracle that builds Java.
My name is Nikolai Parlock and this is the podcast version of Inside Java Newscast number
107 towards better checked exceptions, first published to YouTube on February 19th.
Enjoy the episode!
So why discuss exceptions and particularly checked exceptions now?
Unlike most of the episodes, this one is not triggered by a specific proposal.
It's kind of the other way around, with Java evolving a lot in recent and coming years,
it may well address error handling at some point.
But for that conversation to be fruitful, we must move beyond checked exceptions bad, turn
of please, and there are two parts to that.
First, understanding that within the solutions space for error handling in Java, checked exceptions
are a net positive.
Yes, I like checked exceptions and more than that, I think it's not just subjective preference
but YouTube objective benefits.
But I won't be making that argument here, it has been made countless times before, with
just as many counter-arguments and it takes up a lot of time and energy without getting
us anywhere.
Instead, we'll be operating from the position that checked exceptions remain an integral
part of Java, because they're beneficial or otherwise because Brian gets and enjoys
us suffering either way, they're here to stay.
Which brings us to the second part and the conversation I actually want to have.
One that I believe would be much more fruitful, namely, if checked exceptions are so great,
why do so many developers dislike them?
I mean, even the JDK desks were at some point so flustered that they created unchecked
IO exception.
So what are sharp edges and which one could open JDK realistically file off?
Let's talk about actual besteps instead of just repeating the same flame war.
And this also goes for the comments, by the way.
Okay, with what way well I've been the longest newscast intro ever behind us, let's get
going.
Ready?
Then let's dive right in.
As you know, whether an exception is checked or not depends on where it sits in the
exception type hierarchy.
If it extends runtime exception, it's unchecked.
If it doesn't, it's checked.
And yes, will ignore errors.
Unfortunately, determining this essential property by inheritance collides with other reasons
to build a type hierarchy.
In this case, particularly with unified error handling and access to information.
A good example is SQL exception.
Catching it allows handling all SQL related problems that code can be expected to encounter
and it gives unified access to error codes and the SQL state.
But because it's checked, saw all exceptions that extended and there are a bunch of them
that really shouldn't be.
For example, what's the recourse when catching SQL syntax error exception or SQL invalid
authorization spec exception?
In almost all situations, an SQL syntax error and validity be authentication is a bug
in the program and so the exception should be unchecked.
So when creating an exception type as the root for domain-specific exception hierarchy,
you need to decide for all inheriting exceptions where there'll be checked or not and if you
believe in the value of checked exceptions like OpenJDK does, then you are very likely to
err on the site of making them all checked instead of all unchecked.
All that to say, it would be great if Java could find a more detailed way to mark exceptions
as checked or unchecked, one that allows us to apply that to individual types in larger
hierarchy, for example through a marker interface.
Because here's the thing, that checked exceptions provide value doesn't mean that they don't
have downsides, which means it's important to use them diligently and only where it makes
sense for the code calling a method to handle its error.
Inherited check-at-ness is one reason why there are too many checked exceptions, but it's
not the only one.
It's just overly liberal use of them.
Take input streams and output streams close methods, for example.
They throw the checked IO exception, but what can you possibly do when catching them?
Worse, these two classes and a bunch of their subclasses state that close doesn't even
do anything, so they force you to deal with an exception they never throw because some
subtype might.
No wonder so many of us dislike checked exceptions.
Suboptimal API design can also lead to checked exceptions being more prominent than they
need to be.
API is sometimes bundle functionality, while only one part throws a checked exception.
But without a way to isolate that aspect, every call requires dealing with this exception.
Take JDK methods like string get bytes, for example.
It needs a character set, and in the methods initial form you specify the charset by name,
but what if no such charset exists?
That's why get bytes throws the checked unsupported encoding exception.
So even if the name charset worked well for the other 10 places in your code base, you
still need to handle the checked exception here.
Java 6 fix this by introducing an overload that takes a charset instance.
But interestingly, the static factory method charset4name does not throw a checked exception.
Passing it in a legal name is considered a programming error because there's also the
static method is supported, which you're supposed to use before trying to instantiate a charset.
That successfully decreases the checked exception counter by one.
But the longer I think about it, the less convinced the AM that it really improves anything.
But that's a thought that needs more time to marinate, maybe photo for another video.
For now, let's summarize a few things, libraries, the JDK, as well as others in the ecosystem,
can do to improve the situation.
First, they can revisit which exceptions need to be checked and consider spreading exception
types if necessary.
They may also be able to restructure APIs so that operations that throw checked exceptions
are extracted from those that don't, giving users the chance to isolate error handling in
fewer places, just like with a charset example.
It may also be possible to buffer error states instead of throwing on every call, an example
for this would be print stream and print wider, although these classes do have other issues.
Unfortunately, there's a problem with changing an API to no longer throw a checked exception.
Namely, that such a change is source incompatible.
A tri-catch block that catches a specific checked exception demands that something in the
tri-block declares to throw it, which makes the reduction of checked exceptions daunting,
specifically for JDK APIs.
And it's not like removing a handful of them would move the needle.
For this to have a positive impact, a decent chunk of checked exceptions would have to
turn out to be unnecessary and be removed, each of them a breaking change.
So it would be really helpful for this approach if the compiler could relax a bit and let
us compile code that catches checked exceptions that aren't thrown.
This has its own downsides, of course, so it requires careful consideration.
But it would also help with another aspect that makes checked exceptions annoying.
As soon as you invoke a method that throws, you need to do something to keep the code
compiling.
So far so begrudgingly good.
But then, when you're still in the middle of working on that piece of code and comment
out a method or move things around, and nothing throws the exception anymore, you need
to undo all that.
For checked exceptions, constantly yell at you from the rafters while you're trying to
line up your shot.
Very annoying.
This segment might as well be called just use either.
And while that can help, I'd argue that the utility of this advice is pretty limited.
This video is already running long, so I'm not introducing how either works.
If you don't know, there's a link to an explanation in the description right below the like
button.
First of all, let's observe that a method that returns a value but can throw a checked
exception is functionally the same as a method that returns something like either of that
value or that exception.
But handling an either versus an exception is of course different, and there are two
downsides that in my opinion make either a new solution in Java.
One is that exceptions are much easier to pass up the call stack if you don't want to handle
them right there.
Just call a bunch of methods and add the exceptions to your throws clause.
With either, all those calls need to happen in a functional pipeline, and while like those
more than most, I absolutely don't want all my code to be stuck in them.
The other and much more relevant downside is that either removes the type information from
throws into a generic type, and frankly that sucks because it means that when you chain
operations that can throw different kinds of exceptions, say the first is an IO exception
and the second an SQL exception, the either generic type will quickly escalate to just
exception, which removes a lot of information from the type system.
In most situations, this is not an improvement, but that doesn't mean that there is no room
for either.
Either.
Check the exceptions, build on an implicit assumption of immediacy, an operation can throw
an exception and because you're calling the operation you need to handle the exception.
But what if you're not calling the operation, or at least not directly?
In a stream pipeline for example, you are passing the operation on and something else executes
it later on your behalf.
Now the fact that your operation can produce an error needs to be transported from where
you passed it to where you trigger the execution, for example from a stream map to its list.
You could try to capture the execution types with generics, but you run to the same issue
I described earlier.
And the difficulty or sometimes outright impossibility of transporting the error information is why
checked exceptions and deferred computation don't work well together.
This often comes up as lambdas don't work to check exceptions, but that's mostly missing
the point.
Ping me in the comments if you want me to go into more details on that.
So checked exceptions struggle with transporting type information from registering to executing
a deferred operation.
You know what doesn't?
Either.
Which is why despite its shortcomings, it's very handy in specific situations like
in a stream pipeline.
And I think there are two Java features that could improve this overall problem complex.
Admittedly, the entirely speculative, but hear me out.
First, one could be the ability of the Java compiler to track multiple checked exception
types in a single generic parameter.
That way in either or a beefed upstream could easily collect multiple exceptions.
The language no terminology for this would be variadic generics or union types to different
approaches that could achieve the similar outcomes for our use case.
And the other feature could be a simple way to go from a method that returns and throws
to an either of both and back.
Then API's could stick to declaring return types and checked exceptions, but the users could
easily switch to a form that works better for deferred computation.
This could be a pure library feature, but it could also have a language component where
a try turns it to expression that returns in either.
Even language change in this space that deserves a mention and does have a jet draft is for
switch to gain the ability to handle exceptions that were thrown by the selector expression.
In situations where the error case can be corrected to yield an instance of the same type
as the successful branches, this would be very useful.
It's essentially try as an expression for recoverable cases.
This draft is also linked in the description.
Beyond the changes that could be applied to the language or to libraries, I think our
style can change as well.
For example, it has long been accepted that tests just throw whatever exception they encounter.
If we write scripts or other small programs, we can't just do the same.
For those of us who don't mind functional APIs, we can aggressively replace checked exceptions
with either try or even just an optional.
And in situations where nothing can be done about an error, except letting a high level
handler catch it, wrapping a checked exception in an unchecked one is correct and easy to
do too.
For any of those options, it's straightforward to create helper methods that wrap method
calls accordingly.
Yeah, it's not as low effort as a make everything unchecked switch, but it's also not exactly
burdensome.
Or as we move towards more APIs that rely on pattern matching, we can express some error
cases through domain-specific return types.
Either way, teams absolutely should get together and create a style guide for their project
that matches their domain and preference.
But in the end, I think none of these variants, nor the failure mechanisms of other languages,
by the way, will be easy to work with if you want to do more than just let errors rip.
Because good error handling is hard inherently.
Of course, different mechanisms have different trade-offs, and I genuinely believe that some
can, in aggregate, be better than others.
But that doesn't change the fact that a big chunk of the complexity is essential.
An example is the general difficulty to communicate information across tech frames and abstractions.
Something that plays logging too, by the way.
An unfortunate consequence of that is that there's no Pareto principle at play, where
a few small or easy improvements will yield large results, and anybody claiming that probably
has not thought about the problem for very long.
Instead, it seems that a lot of hard work is required to move the needle.
So let's talk about and tackle that hard work.
Let me know in the comments what changes you think would make checked exceptions more
usable.
Other than that, I'll see you again in two weeks.
I need to hurry up and wait to catch my train.
So long.



