Exception Handling in Java
Today i want to talk about exception handling in Java. I know a lot of folks have shared about this in the past, but I screw it here’s one more voice. This blog won’t be about if I think they’re good or bad in Java (it’s a complex subject, and ultimately they aren’t going anywhere), but instead how and where i think they should be used.
A refresher
For a quick refresher lets go over the two main types of exceptions in Java:
- Check exceptions - any exception that must be caught or declared to be caught.
- Unchecked exceptions - any exception that doesn’t need to be caught or declared to be caught.
Examples of Checked exceptions:
public void exampleMethod () throws ExampleChecked {
throw new ExampleChecked();
}
public void exampleMethod () {
try {
throw new ExampleChecked();
}catch(ExampleChecked ex) {
// Handle exception here
}
}
An example of Unchecked exception:
public void exampleMethod () {
throw new ExampleRuntime();
}
Cool, all on the same page. Now lets go into where and how I’d use them.
Checked exceptions
Checked exceptions are both excellent and an absolute pain in the arse.
I use checked exceptions when writing libraries, as a libraries API should be explicit and self documenting. As developers we should avoid surprising users with unexpected side effects. Of course, you should also properly document your library with Java Docs and Wikis, but there’s nothing quite like saying “Hey, this method you’re going to use? It could explode with this and you must handle it”.
To complement this, I avoid unchecked exceptions. I aim to wrap these, and re-throw them as a checked one. This way I can control the messaging and the stack trace to ensure the best language and structure. Remember: Your exceptions are part of your API.
Also I don’t let generic Java ones escape without some level of catch and re-throw. It’s a pain trying to understand where a NullPointerException
came when it’s not been wrapped in a something specific to the library that’s imported.
On the flip side, I avoid putting these into my application code unless absolutely required / desired. There are two main reasons:
- I’ll have to code every method to bubble up the exception to a root exception handler. Very messy.
- Java Optionals and Streams don’t work overly well with Checked Exceptions (though, unchecked exceptions are still pretty ugly but I don’t have to code around them).
More on these below.
Unchecked exceptions
I’d argue these are, on the whole, more useful and less annoying than checked exceptions. They allow a developer (me) to be lazy in both the best and worst ways.
I spend a lot of my time in Spring Boot / web applications. So I use generic error handlers that look out for extensions of the RuntimeException
class. So if one is throw, a 500 internal server error is automatically returned. Or if a UserNotFoundException
is throw, then a 404 is returned. And so on.
This makes the applications I write to be a little easier to follow / have a generic safety net. It also means every method doesn’t have a load of declared exceptions because some item of code throws one 7 methods deep. This is kinda what I mean when it allows me to be lazy. That generic handler does a lot of the leg work.
So I avoid checked exceptions in my application code.
However, there is a bad way to be lazy here; I can throw them for everything. I’ve seen in a number of places where a devs catch a RuntimeException
and then do something else. When I was younger, I certainly did this. However, this is expensive and a lot of additional lines for very little gain.
Here’s an example where getUser
throws a UserNotFoundException
when no user is found:
public User getUser () {
try {
return getUser(userId);
} catch(UserNotFoundException ex) {
return performUserSetup();
}
}
In cases like this I would instead use Optionals
, and a mix of or
, orElseGet
and so on. This makes for much cleaner code. More efficient code. And no need for any exceptions at all.
public User getUser () {
this.findUser("someUserId")
.or(() -> performUserSetup())
.orElseThrow(RuntimeException::new);
}
On a random side note; whenever I write methods that return an Optional
, I tend to use find
or something like that.
TL;DR
So, in summary here’s my approach:
Use checked exceptions in libraries as code should self document and shouldn’t surprise a developer.
Use unchecked exceptions in application code to avoid writing methods with huge signatures to leverage root exception handlers, and leverage
Optional
where possible
Happy programming!