This is a mobile version, full one is here.
Yegor Bugayenko
28 July 2015
Checked vs. Unchecked Exceptions: The Debate Is Not Over
Do we need checked exceptions at all? The debate is over, isn’t it? Not for me. While most object-oriented languages don’t have them, and most programmers think checked exceptions are a Java mistake, I believe in the opposite—unchecked exceptions are the mistake. Moreover, I believe multiple exception types are a bad idea too.
Let me first explain how I understand exceptions in object-oriented programming. Then I’ll compare my understanding with a “traditional” approach, and we’ll discuss the differences. So, my understanding first.
Say there is a method that saves some binary data to a file:
public void save(File file, byte[] data)
throws Exception {
// save data to the file
}
When everything goes right, the method just saves the data and returns control.
When something is wrong, it
throws
Exception
and we have to do something
about it:
try {
save(file, data);
} catch (Exception ex) {
System.out.println("Sorry, we can't save right now.");
}
When a method says it throws
an exception, I understand that the
method is not safe. It may fail sometimes, and it’s my responsibility to
either 1) handle this failure or 2) declare myself as unsafe too.
I know each method is designed with a
single responsibility principle
in mind. This is a guarantee to me that if method save()
fails, it means
the entire saving operation can’t be completed. If I need to know what
the cause of this failure was, I will un-chain
the exception—traverse the stack of chained exceptions and stack traces
encapsulated in ex
.
I never use exceptions for flow control, which means I never recover
situations where exceptions are thrown. When an exception occurs, I let it float up to the
highest level of the application. Sometimes I
rethrow
it in order to add more semantic information to the chain. That’s why it doesn’t matter
to me what the cause of the exception thrown by save()
was. I just know
the method failed. That’s enough for me. Always.
For the same reason, I don’t need to differentiate between different exception
types. I just don’t need that type of hierarchy. Exception
is enough for me.
Again, that’s because I don’t use exceptions for flow control.
That’s how I understand exceptions.
According to this paradigm, I would say we must:
- Always use checked exceptions.
- Never throw/use unchecked exceptions.
- Use only
Exception
, without any sub-types. - Always declare one exception type in the
throws
block. - Never catch without rethrowing; read more about that here.
This paradigm diverges from many other articles I’ve found on this subject. Let’s compare and discuss.
Runtime vs. API Exceptions
Oracle says some exceptions should be part of API (checked ones) while some are runtime exceptions and should not be part of it (unchecked). They will be documented in JavaDoc but not in the method signature.
I don’t understand the logic here, and I’m sure Java designers don’t
understand it either. How and why are some exceptions important while
others are not? Why do some of them deserve a proper API position
in the throws
block of the method signature while others don’t? What is the criteria?
I have an answer here, though. By introducing checked and unchecked exceptions, Java developers tried to solve the problem of methods that are too complex and messy. When a method is too big and does too many things at the same time (violates the single responsibility principle), it’s definitely better to let us keep some exceptions “hidden” (a.k.a. unchecked). But it’s not a real solution. It is only a temporary patch that does all of us more harm than good—methods keep growing in size and complexity.
Unchecked exceptions are a mistake in Java design, not checked ones.
Hiding the fact that a method may fail at some point is a mistake. That’s exactly what unchecked exceptions do.
Instead, we should make this fact visible. When a method does too many things, there will be too many points of failure, and the author of the method will realize that something is wrong—a method should not throw exceptions in so many situations. This will lead to refactoring. The existence of unchecked exceptions leads to a mess. By the way, checked exceptions don’t exist at all in Ruby, C#, Python, PHP, etc. This means that creators of these languages understand OOP even less than Java authors.
Checked Exceptions Are Too Noisy
Another common argument against checked exceptions is that they make our
code more verbose. We have to put try/catch
everywhere instead of
staying focused on the main logic. Bozhidar Bozhanov even
suggests
a technical solution for this verbosity problem.
Again, I don’t understand this logic. If I want to do something when method
save()
fails, I catch the exception and handle the situation somehow.
If I don’t want to do that, I just say my method also throws
and pay no
attention to exception handling. What is the problem? Where is the verbosity
coming from?
I have an answer here, too. It’s coming from the existence of unchecked
exceptions. We simply can’t always ignore failure, because the interfaces
we’re using don’t allow us to do this. That’s all. For example, class
Runnable
,
which is widely used for multi-thread programming, has
method run()
that is not supposed to throw anything. That’s why we always have
to catch everything inside the method and rethrow checked
exceptions as unchecked.
If all methods in all Java
interfaces would be declared either as
“safe” (throws
nothing) or “unsafe” (throws Exception
), everything would
become logical and clear. If you want to stay “safe,” take responsibility
for failure handling. Otherwise, be “unsafe” and let your users worry about
safety.
No noise, very clean code, and obvious logic.
Inappropriately Exposed Implementation Details
Some say
the ability to put a checked exception into throws
in the method
signature instead of catching it here and rethrowing a new type
encourages us to have too many irrelevant exception types in method
signatures. For example, our method save()
may declare that it
may throw OutOfMemoryException
, even though it seems to have
nothing to do with memory allocation. But it does allocate some memory, right?
So such a memory overflow may happen during a file saving operation.
Yet again, I don’t get the logic of this argument.
If all exceptions are checked, and we don’t have multiple exception types,
we just throw Exception
everywhere, and that’s it. Why do we need to care
about the exception type in the first place? If we don’t use exceptions to control
flow, we won’t do this.
If we really want to make our application memory overflow-resistant, we will
introduce some memory manager, which will have something like the bigEnough()
method, which will tell us whether our heap is big enough for the next
operation. Using exceptions in such situations is a totally inappropriate approach
to exception management in OOP.
Recoverable Exceptions
Joshua Bloch, in Effective Java, says to “use checked exceptions for recoverable conditions and runtime exceptions for programming errors.” He means something like this:
try {
save(file, data);
} catch (Exception ex) {
// We can't save the file, but it's OK
// Let's move on and do something else
}
How is that any different from a famous anti-pattern called Don’t Use Exceptions for Flow Control? Joshua, with all due respect, you’re wrong. There are no such things as recoverable conditions in OOP. An exception indicates that the execution of a chain of calls from method to method is broken, and it’s time to go up through the chain and stop somewhere. But we never go back again after the exception:
App#run()
Data#update()
Data#write()
File#save() <-- Boom, there's a failure here, so we go up
We can start this chain again, but we don’t go back after throw
. In other
words, we don’t do anything in the catch
block. We only report the problem
and wrap up execution. We never “recover!”
All arguments against checked exceptions demonstrate nothing but a serious misunderstanding of object-oriented programming by their authors. The mistake in Java and in many other languages is the existence of unchecked exceptions, not checked ones.