Notes on exception handling
Wednesday, November 20th, 2013If a function be advertised to return an error code in the event of difficulties, thou shalt check for that code, yea, even though the checks triple the size of thy code and produce aches in thy typing fingers, for if thou thinkest “it cannot happen to me”, the gods shall surely punish thee for thy arrogance.
Henry Spencer’s 10 Commandments for C Programmers
In the olden days, before Exceptions were invented, we wanted to write code like this:
// Called when the user presses the "triple" button
void onTripleButtonPressed() {
String valueEntered = readValueFromField();
int valueToDisplay = triple(stringToInteger(valueEntered));
displayResult(integerToString(valueToDisplay));
}
int triple(int value) {
return value * 3;
}
Unfortunately, we were forced to write code like this instead:
int onTripleButtonPressed() {
StringHolder valueEntered = new StringHolder();
int resultCode = readValueFromField(valueEntered);
if (isError(resultCode))
return resultCode;
IntHolder valueAsInteger = new IntHolder();
resultCode = stringToInteger(valueAsInteger, valueEntered.value);
if (isError(resultCode))
return resultCode;
resultCode = triple(valueAsInteger);
if (isError(resultCode))
return resultCode;
StringHolder valueToDisplay = new StringHolder();
resultCode = integerToString(valueToDisplay, valueAsInteger.value);
if (isError(resultCode))
return resultCode;
resultCode = displayResult(valueToDisplay.value);
if (isError(resultCode))
return resultCode;
return OK; // :-)
}
private int triple(IntHolder valueAsInteger) {
int result = valueAsInteger.value * 3;
if (isOverflow(result))
return ERROR_OVERFLOW;
valueAsInteger.value = result;
return OK;
}
Yes, we were FORCED to check the result of each and every operation. There was no other way to write reliable software. As you can see:
- Code size is more than three times
- Logic becomes obscure
- Functions are forced to return two values; the intended result AND an error code.
- You always, always, always had to return an error code from all functions.
There was no alternative, until exceptions came along. Then we were finally able to write simple code in a reliable way:
// Called when the user presses the "triple" button
// Exceptions are handled here
void onTripleButtonPressed() {
try {
tryOnTripleButtonPressed();
} catch (Exception e) {
alertUser(e.getMessage());
}
}
// Business logic is handled here
private void tryOnTripleButtonPressed() {
String valueEntered = readValueFromField();
int valueToDisplay = triple(stringToInteger(valueEntered));
displayResult(integerToString(valueToDisplay));
}
In this example, all exception handling is centralized at the point where the GUI gives control to our code. The code that performs what the user really wanted is in another function, which contains exactly the same clean code of the first example.
Thus, the invention of exception handling allows us to cleanly separate code that performs the happy path, and code that handles the many possible exceptional conditions: integer overflow, I/O exceptions, GUI widgets being improperly configured, etc.
What you saw in the previous example is the
Fundamental Pattern of Exception Handling: centralize exception handling at the point where the GUI gives control to our code. No other exception handling should appear anywhere else.
It turns out that the Fundamental Pattern of Exception Handling is the only pattern that we need to know. There are other cases where we are tempted to write a try-catch, but it turns out that we nearly always have better ways to do it.
Antipattern: nostalgia (for the bad old days)
There is an ineffective style of coding that we sometimes see in legacy code:
void onTripleButtonPressed() {
String valueEntered = null;
try {
valueEntered = readValueFromField();
} catch (Exception e) {
logger.log(ERROR, "can't read from field", e);
return;
}
int valueEnteredAsInteger = 0;
try {
valueEnteredAsInteger = Integer.parseInt(valueEntered);
} catch (Exception e) {
logger.log(ERROR, "parse exception", e);
return;
}
int valueToDisplay = 0;
try {
valueToDisplay = triple(valueEnteredAsInteger);
} catch (Exception e) {
logger.log(ERROR, "overflow", e);
return;
}
try {
displayResult(integerToString(valueToDisplay));
} catch (Exception e) {
logger.log(ERROR, "something went wrong", e);
return;
}
}
As you can see, this is just as bad as the olden days code! But while in the old times we had no alternatives, now we do. Just use exceptions the way they were intended to.
Stay up no matter what
Sometimes there is a feeling that we should write code that “stays up no matter what happens”. For instance:
private void tryOnTripleButtonPressed() {
String valueEntered = readValueFromField();
int valueToDisplay = triple(stringToInteger(valueEntered));
try {
sendEmail(userEmail, "You tripled " + valueEntered);
} catch (Exception e) {
logger.log(WARNING, "could not send email", e);
// continue
}
displayResult(integerToString(valueToDisplay));
}
Here we have added an email confirmation; whenever the user presses the button, they will also receive an email. Now we should ask ourselves which of the two:
- is the sending of the email an integral and fundamental part of what should happen when the user presses the button?
- Or is it an accessory part should never stop the completion of the rest of the action?
This is a business decision. If the business decides on 1., then we should remove the try-catch. By applying the Fundamental Pattern, proper logging of the exception will be done elsewhere. We end up with:
private void tryOnTripleButtonPressed() {
String valueEntered = readValueFromField();
int valueToDisplay = triple(stringToInteger(valueEntered));
sendEmail(userEmail, "You tripled " + valueEntered);
displayResult(integerToString(valueToDisplay));
}
Clean code again. Yay!
If the business decides on 2., then we cannot allow sendMail to throw exceptions that might stop the processing of the user action. What do we do now? Do we have to keep the try-catch?
The answer is yes, but out of the way. There are two ways to do this: the easy way and the simple way. If you do it the easy way, you will move the try-catch inside the sendMail function. You will get code like this:
private void tryOnTripleButtonPressed() {
String valueEntered = readValueFromField();
int valueToDisplay = triple(stringToInteger(valueEntered));
sendMail(userEmail, "You tripled " + valueEntered);
displayResult(integerToString(valueToDisplay));
}
private void sendMail(EmailAddress email, String message) {
try {
trySendEmail(email, message);
} catch (Exception e) {
logger.log(WARNING, "could not send email", e);
// continue
}
}
You have moved exception handling for sending mail to a dedicated function (good). However, you still have complicated code tightly coupled to the user action. The sending of email, which is an accessory operation, makes understanding the fundamental operation more difficult (bad!)
What is the simple way, then? In the simple way we eliminate the tight coupling betweeen tripling the number and sending the email. There are several patterns that we might use; a typical choice would be “publish-subscribe”. We set up a subscriber that waits for the “User Pressed the Triple Button” event. In the main onTripleButtonPressed function we don’t know nor care what the subscriber does. It might send email, write logs, compute statistics, or maybe distribute the event to a list of other subscribers. We don’t know nor care! The code looks like this:
private void tryOnTripleButtonPressed() {
String valueEntered = readValueFromField();
int valueToDisplay = triple(stringToInteger(valueEntered));
subscriber.notifyTripled(valueEntered);
displayResult(integerToString(valueToDisplay));
}
In the Simple way, the Fundamental Pattern has been respected: for the subscriber object, the start of the processing is within the notifyTripled method. We have clean and loosely coupled code.
Avoid resource leaking
Another time when we are tempted to use a try-catch in violation of the Fundamental Pattern is when we open some resource that must be closed.
void doSomethingImportant() {
Reader reader = null;
try {
reader = new FileReader("foo bar");
doSomethingWith(reader);
reader.close();
} catch (Exception e) {
if (null != reader) {
reader.close();
}
// report the exception to the callers
throw new RuntimeException(e);
}
}
It is correct that we close the reader in the event that something within doSomethingWith throws an exception. But we don’t want the catch; a finally clause is better:
void doSomethingImportant() throws IOException {
Reader reader = null;
try {
reader = new FileReader("foo bar");
doSomethingWith(reader);
} finally {
if (null != reader)
reader.close();
}
}
This way, we don’t even need to worry about rethrowing the exception to our callers. The code is both correct and clear.
Irritating APIs
Some APIs force us to use more try-catches that we’d like. An example in Java is how to read data from a database using JDBC:
PreparedStatement statement = connection.prepareStatement("...");
try {
ResultSet results = statement.executeQuery();
try {
while (results.next()) {
// ...
}
} finally {
results.close();
}
} finally {
statement.close();
}
Here the problem lies in how the JDBC API is defined. We can’t change it of course, but we should encapsulate all uses of the JDBC API in a single class, so that we don’t have to look at this code anymore. Treat that class as your adapter to JDBC. For instance:
Results results = new Database(connection).read("select * from foo");
// use results
Of course our object is a lot less flexible than the JDBC API. This is expected: we want to decide how we use JDBC and encapsulate that “how” in a single place.