In our BDD course, we use a real example of an online library patron portal. Course participants are provided with a number of (often vague) business rules regarding whether or not a book loan can be renewed. Some have to do with the state of the book (e.g., has another patron requested a “hold” on the book?) and others on the loan, itself (e.g., has this patron already renewed twice?).
The business rules shape the Gherkin scenarios, to be sure, but that’s not what I want to talk about today. After the scenarios, step definitions, and glue code are all written, it came down to writing the implementation, and one course participant brought up an excellent question.
“If the book isn’t renewable, should I throw an exception?”
And, as any good consultant would answer, I said, “Well, it depends…”
What it depends on, is the customer. If we’re building a stand-alone app, a smartphone app, or a web application, then using exceptions to communicate business rules and user mistakes is strongly discouraged.
If we’re instead building a Web API, JAR file, or .DLL for another team, or an external client; then perhaps an exception is appropriate…but even then, perhaps not. It’s better, I would say, to encourage client software to avoid exceptions.
Okay, here’s what many developers typically want to write…
And, what would the caller of this code be expected to do? Yup, catch the exception and deal with it, somehow.
1. Just let it bubble up at least one level.
If it’s a Java “checked exception” this is done by declaring that the caller also throws the exception. Eventually checked exceptions have to be dealt with, though it’s never quite clear when, where, or what that’s supposed to be. Retry with the exact same data? Reboot the phone? Reboot the Internet?
You’ve likely seen methods (you know, other people’s methods) that throw exceptions from three or four different libraries: A strong code smell indicating lots of unintentional coupling within that method. Or, if the Java developer gets really lazy, the method will throw Throwable. 🙁
If it’s a Java RuntimeException, or a .Net Exception (Microsoft decided not to implement checked exceptions in .Net.), you can still declare that your method throws it (and you probably should, if you really throw it) but your client is not obligated to know what to do with it.
2. Catch, Wrap, and Throw
3. Log the exception, then act as though everything is fine.
What Exceptions Were Meant to Do
Exceptions were intended to provide an alternate return path out of a method (function, procedure) when something very out-of-the-ordinary happens.
Exceptions are meant to communicate to developers, not users. They can indicate a defect in the method’s code (due to insufficient unit testing), or (perhaps) in the caller’s code (also due to insufficient unit testing). They can also indicate when the expected flow of the system is thwarted by some other service (database, network, filesystem) that is not currently functioning. They should not be thrown due to business rules or problems with user-provided data.
And please never, ever, ever show the Exception’s error message and stack trace to the user. (I once sent a note to a website explaining to them that the connection pool they were using to connect to Oracle via JDBC was likely not threadsafe. I think they responded by deleting my login. Fine by me…)
An example of an inappropriate use for an exception: When the user’s credit card has expired.
An example of when an exception make more sense: When the credit card verification API is not responding.
In that case, I would suggest including pertinent in-scope data in the exception message, like the URL or other connection data used. That could include sensitive data: Yet another reason not to share it beyond your own team, and servers.
Generally, exceptions should halt the activity that was supposed to occur, rather than letting things continue blindly, as though nothing untoward had occurred. The code that does catch the exception generally does so in order to (a) clean up, rewind, roll-back, or otherwise undo any partial steps that were completed, and/or (b) tell the user the bad news that what they were trying to accomplish was unsuccessful, perhaps with some useful suggestions.
Try, Try Again?
People will often talk about the ability to retry when an exception is thrown. I’ve rarely seen that functionality automated successfully. Usually, it’s left to the user, and that’s been the better choice. Making the user wait, while your code uses a tight loop to repeatedly bang on their unresponsive GPS chip, while deep within the Ozarks, is just going to make them impatient. And run down their battery.
The best software products I worked on generally handled exceptions similarly. There was usually a database or filesystem involved: The activity was a purchase, the storing of a new collection of records, or the altering of a set of related records. So if something blew up in our faces, the database exception would typically be caught and wrapped at the source (e.g., a JDBC call), then our wrapper exception would bubble all the way up to a handler that would give the user some context and suggestions.
E.g., “Very sorry. We could not complete your purchase of War and Peace. It has been moved from your Shopping Cart to your Saved4Later list (seen whenever you view your cart). You may want to call the help desk at 800-555-5555. If so, please refer to your specific issue #XABCXYZ. Or, you may choose to try again another time and see if it occurs again. Meanwhile, we’ve created this Order with the other items from your cart.”
Typically, that means that all unrecoverable exceptions (which is most of them) need to bubble up to the level where they can be dealt with honestly: Log them, let the development team know that an exception has occurred, and let the user know that their activity was unsuccessful.
In Java, we once developed a self-logging application exception. Upon construction, it would write its message, unique issue number, and stack trace to the log; all before being thrown. It would also send e-mail containing the issue number to the development team’s support address. And, if another exception occurred while trying to do all that, well then that would pretty much crash the app.
And, isn’t that what you want to happen? No one wants to be using your software as it flails around in cascading layers of crashing, untested spaghetti code.
When I tell developers about the automatic exception e-mail (or, in one case, the logger would trigger the pager), they often complain: “We’d get thousands of messages daily!”
I respond with “I’m so sorry. So very, very sorry. How many unit tests did you say you have…?” I can be a real jerk when it comes to unit tests. But…
Exceptions should be…exceptional!
An Alternative to Exceptions
Okay, so what should happen when someone tries to renew a book that has a hold request? The general “pattern” I suggest is to encourage the caller to query, then act. In other words:
For some reason, this bothers a lot of developers. I think we are often looking for the perfect independent, object-oriented, single-method solution to every software activity. It’s a red herring.
Or, a better metaphor would be the Unicorn: One horn to rule them all, and in the rainbow, unite them; or some such. As much as we want to embrace Single Responsibility Principle, every computer I ever worked on had a testandset instruction of some kind. I consider “and” in a method name to be a code smell; and yet there it is, physically etched into each computer’s brain!
So, good for you, endeavoring to follow the S.O.L.I.D. principles. They provide great guidelines. But also recall that “perfection is the enemy of good.” (That’s particularly important to remember when you get to the O in S.O.L.I.D., the Open-Closed Principle, but that’s perhaps fodder for a future post…)
Good objects interact with each other. That’s because one of the real purposes of object-oriented development is to make it easy to understand and alter one small cluster of data and behavior (an object) without affecting other less-related clusters of data and behavior. Clear, maintainable, useful objects rely on each other, but aren’t all blended together into one big ball of spaghetti.
If we compare the code above to…
…well, which one better communicates the code’s intent to you and your teammates?
Lastly, if I’m the person writing both the called and the calling code, I won’t need to throw any exceptions in my renew() code, because I’m going to write a unit test that makes certain I always call loan.isRenewable() before calling loan.renew(). If I test that I didn’t make a mistake, do I need to check again in the code for the same stupid programming mistakes?