S73417H S73417H - 1 month ago 11
Java Question

Checked vs. Unchecked Exceptions in Service Layer

I work on a project with a legacy service layer that returns null in many places if a requested record does not exist, or cannot be accessed due to the caller not being authorized. I am talking about specific records requested by ID. For instance, something like:

UserService.get(userId);


I have recently pushed to have this API changed, or supplemented with a new API that throws exceptions instead. The debate over checked vs unchecked exceptions has ensued.

Taking a note from the designers of JPA/Hibernate et all., I have suggested that unchecked exceptions may be most appropriate. My argument being that users of the API cannot be reasonably expected to recover from these exceptions and in 99% of the cases we can at best notify the application user that some error has occurred.

Having runtime exceptions propagate up to generic handling mechanisms will obviously reduce a lot of the complexity and required branch handling involved in dealing with edge-case exceptions. But, there is a lot of concern surrounding such an approach (rightly so).

Why have the designers of such projects as JPA/EJB and Hibernate selected to go with an unchecked exception model? Is there a very good justification for it? What are the pros/cons. Should developers using these frameworks still handle the runtime exceptions close to where they are thrown with something like adapter wrappers?

I hope answers to these questions can help us to make the "right" decision regarding our own service layer.

Answer

Though I agree with the view that unchecked exceptions make for a more convenient API, that's not to my mind the most important benefit. Instead it's this:

Throwing unchecked exceptions helps you avoid serious bugs.

Here's why. Given ten developers forced to deal with checked exceptions, you are going to get twenty different strategies for dealing with them, many of which are totally inappropriate. Here are some of the more common bad approaches:

  • Swallow. Catch the exception and completely ignore it. Keep on going as if nothing happened even though the app is now in an unstable state.
  • Log and swallow. Catch the exception and log it, thinking that now we're being responsible. Then keep on going as if nothing happened.
  • Mystery default. Catch the exception, and set some field to some default value, usually without telling the user about it. For example, if you can't load some user's roles, just pick a low-privilege role and assign it. User wonders what is going on.
  • Silly/dangerous mystery default. Catch the exception, and set some field to some really bad default value. One example I saw in real life: can't load a user's roles, so go ahead and assume the best (i.e. give them a high-privilege role so as not to inconvenience anybody).
  • Misreport. The developer doesn't know what the exception means and so just comes up with his own idea. IOException becomes "Can't connect to the server" even if establishing a connection has nothing to do with the issue.
  • Mask via broad catch, and misreport. Try to clean up the code by catching Exception or (ugh) Throwable instead of the two checked exceptions that the method actually throws. The exception handling code makes no attempt to distinguish (say) resource availability issues (e.g. some IOExceptions) from outright code errors (e.g. NullPointerException). Indeed it often picks one of the exceptions arbitrarily and misreports every exception as being of that type.
  • Mask via huge try, and misreport. A variant of the previous strategy is to put a whole bunch of exception-declaring calls in the scope of a single large try block, and then catch either Exception or Throwable because there's nothing else that would handle all the exceptions being thrown.
  • Abstraction-inappropriate rethrow. Rethrow the exception even if the exception is inappropriate to the abstraction (e.g. rethrowing a resource-related exception from a service interface that's supposed to hide the resource).
  • Rethrow without wrapping. Rethrow an exception (either unchecked or else abstraction-appropriate checked), but simply drop the nested exception that would give anybody a chance to actually figure out what is going on.
  • Dramatic response. Respond to a nonfatal exception by exiting the JVM. (Thanks to this blog post for this one.)

In my experience it is quite a bit more common to see the approaches above than it is to see a correct response. Many developers--even "senior" developers--have this idea that exceptions must be suppressed at all costs, even if it means running the app in an unstable state. That is dangerously wrong.

Unchecked exceptions help avoid this issue. Developers who don't know how to handle exceptions tend to see exceptions as inconveniences to overcome, and they don't go out of their way to catch them. So the exceptions just bubble up to the top where they offer up a stack trace and where they can be handled in a consistent fashion. In the rare case where there's actually something better to do than let the exception bubble up, there's nothing stopping you.

Comments