Effective java : Exceptions

Exceptions

Objective: This section provides guidelines for how to use exceptions effectively. When they are used properly, they can improve a program’s readability, reliability, and maintainability; but when they are used poorly, they can have the opposite effect.

Key topics:

  1. Exceptions for Exceptional Conditions Only
  2. Checked Exceptions and Unchecked Exceptions
  3. Standard Exceptions
  4. Exception Translation
  5. Exception Implementation
  6. Exceptions and Failure Atomicity

Estimated time: 15-30 minutes.

Exceptions for Exceptional Conditions Only

Which of the following implementations would be preferable? Why?

 Use exceptions

public void displayCards() {

try {
int i = 0;
while (true) {
cards[i++].displayMe();
}
} catch (ArrayIndexOutOfBoundsException e) {

}
}

 Don’t use exceptions

public void displayCards() {

for (Card card : cards) {
card.displayMe();
}

}

Exceptions should be used only for exceptional conditions, as their name implies. A well-designed API should never force its clients to use exceptions for ordinary control flow. The second implementation above would be much cleaner than the first one.

Checked Exceptions and Unchecked Exceptions

Would the following implementation be appropriate?

/**
* Returns MarketplaceInfo of a given marketplace.
* @throws NotFoundException if marketplaceId is not found; do not retry.
* @throws ServiceUnavailableException if MarketplaceService does not respond after 3 retries.
*/
public MarketplaceInfo getMarketplaceInfoById(MarketplaceId marketplaceId) {
try {
return getMarketplaceInfoByIdFromLocalCache(marketplaceId);
} catch (IOException e) {
MarketplaceInfo info = getMarketplaceInfoByIdFromRemoteCache(marketplaceId);
putMarketplaceInfoToLocalCache(marketplaceId, info);
return info;
} catch (IOException e) {
for (int numRetries = 0; numRetries < 3; numRetries++) {
try {
// Call dependent service to get marketplace info
MarketplaceInfo info = marketplaceService.getMarketplaceInfoById(marketplaceId);
putMarketplaceInfoToLocalCache(marketplaceId, info);
putMarketplaceInfoToRemoteCache(marketplaceId, info);
return info;
} catch (ServiceUnavailableException e) {
sleep(5); // sleep 5 seconds before retry
} catch (NotFoundException e) {
LOG.error(“Unable to get marketplace info because marketplace id {} is not found.”, marketplaceId);
throw e;
}
}
throw new ServiceUnavailableException(“Unable to get marketplace info after 3 retries.”);
}
}

 Yes
 No

While designing/implementing an API, you should throw checked exceptions, a subclass of  Exception , for recoverable conditions and unchecked exceptions, a subclass of  RuntimeException , for programming errors. When in doubt, throw unchecked exceptions. When throwing checked exceptions, add methods to aid in recovery for clients. Note also that when used sparingly, checked exceptions can increase the reliability of programs; but when overused, they make APIs painful to use.

You should declare checked exceptions individually and document precisely the conditions under which each exception is thrown, by using Javadoc  @throws  tag. If the same exception is thrown by many methods in a class for the same reason then you can document it in the class’s documentation comment. In addition, it is particularly important to document unchecked exceptions of methods in interfaces they may throw; do not, however, use throws  keyword on unchecked exceptions.

If you are a caller of an API, don’t ignore any exception, whether it is checked or unchecked, unless you have a good reason to do so. And if you choose to ignore it then you should name the variable  ignored  and add a comment to the catch block to explain why it is appropriate to do so.

The implementation above would be appropriate and show how good the design of APIs used the method could be. Firstly,  IOException  is a checked exception, which requires clients to check it and provide recovery actions if possible. Secondly,  ServiceAvailableException  is an unchecked exception, which doesn’t require clients to check it but they could implement retry actions if they want to. Finally,  NotFoundException , another unchecked exception, indicates precondition violations or programming errors, meaning a failure by the client of the API to adhere to the contract established by this API specification. This type of conditions is unrecoverable; retrying is just useless for clients and makes your service and its dependencies worse while they are struggling.

Standard Exceptions

Which of the following implementations would be preferable? Why?

 Use my exceptions

public class MyIndexOutOfBoundsException extends IndexOutOfBoundsException {

}

public static int searchValueAfterIndex(int[] a, int target, int startIndex) {
try {

} catch (MyIndexOutOfBoundsException e) {

}
}

 Use standard exceptions

public static int searchValueAfterIndex(int[] a, int target, int startIndex) {
try {

} catch (ArrayIndexOutOfBoundsException e) {

}
}

Note that a key difference between expert and novice software engineers is that the formers strive for and usually achieve a high degree of code reuse. Exceptions are no exception to that rule. The Java libraries have already provided a set of exceptions that covers most of the exception-throwing needs of most APIs. You should reuse standard exceptions unless you have a very good reason to implement a new one. Here are some whys:

  • Your APIs will be easier to read, learn, and use because many programmers have already been familiar with standard exceptions.
  • Fewer exception classes mean a smaller memory footprint and less time spent to load classes.
  • Exceptions are serializable, and thus would require some care to write them.

You should not reuse  Exception  or  RuntimeException  directly, but it is fine to reuse the following ones:

  •  NullPointerException : if parameter value is null where prohibited.
  •  IndexOutOfBoundsException : if index parameter value is out of range. If you want more specific then use  ArrayIndexOutOfBoundsException  instead.
  •  UnsupportedModificationException : if object doesn’t support method.
  •  ArithmeticException : if an exceptional arithmetic condition has occurred (e.g., divide by zero).
  •  NumberFormatException : if a string cannot be converted to a number (e.g., try to convert “12y34” to a number).
  •  ConcurrentModificationException : if concurrent modification of an object has been detected where it is prohibited.
  •  IllegalArgumentException : if any non-null parameter value is inappropriate (e.g., a negative number is passed to a non-negative parameter).
  •  IllegalStateException : if the invocation is illegal because of the state of receiving object or if no argument values would have worked (e.g., if the caller attempted to use some objects before it had been properly initialized).

Exception Translation

Which of the following implementations would be preferable? Why?

 With exception translation

public E get(int index) {
ListIterator<E> it = listIterator(index);
try {
return it.next();
} catch (NoSuchElementException) {
throw new IndexOutOfBoundsException(“Index: ” + index);
}
}

 Without exception translation

public E get(int index) throws NoSuchElementException {
ListIterator<E> it = listIterator(index);
return it.next();
}

In general, higher layers should catch lower-level exceptions and, in their place, throw exceptions that can be explained in terms of the higher-level abstraction, unless the lower-level exceptions have been already appropriate to the higher-level ones. This idiom is known as exception translation, whose main purpose is to make the exception consistent with the behavior of its API, but not of its dependences’. So, the first implementation above would be preferable. In some cases where the lower-level exception might be useful to someone debugging the problem that caused the higher-level exception, then you can pass the cause to the higher-level exception. For example:

try {

} catch (LowerLevelException cause) {
throw new HigherLevelException(cause);
}

While exception translation is better than mindless propagation of exceptions from lower layers, you should not overuse it. Where possible, the best way to handle exceptions from lower layers is to avoid them, by ensuring that the lower-level methods succeed. For instance, you can check the validity of the higher-level method’s parameters before passing them to the lower-level method.

Exception Implementation

Which of the following implementations would be preferable? Why?

 Do not provide detail messages

public IndexOutOfBoundsException () {
super(“Unable to continue due to index out of bounds.”);
}

 Provide detail messages

public IndexOutOfBoundsException (int lowerBound, int upperBound, int index) {
super(String.format(“Lower bound: %d, upper bound: %d, but index: %d.”, lowerBound, upperBound, index));
// Save failure information for programmatic access
this.lowerBound = lowerBound;
this.upperBound = upperBound;
this.index = index;
}

Note that the system automatically prints out an exception’s stack trace when it is thrown. This stack trace contains the exception’s class name and detail message. The latter is usually the only information that software engineers will have when they investigate a program failure. So, it is very important that you provide as much crucial information as possible to help them out; of course, do not provide annoying/unnecessary information. Here are some best practices:

  • The detail message of an exception should contain the values of all parameters and fields that have contributed to the exception.
  • Do not include any sensitive information such as passwords, encryption keys, customer names and emails, and so on in the detail message.

Exceptions and Failure Atomicity

Which of the following implementations of stack popping would be preferable? Why?

 Do not pre-check conditions

public Object pop () {
Object result = elements[–size];
elements[size] = null; // Eliminate obsolete reference
return result;
}

 Pre-check conditions

public Object pop () {
if (size == 0) {
throw new EmptyStackException();
}
Object result = elements[–size];
elements[size] = null; // Eliminate obsolete reference
return result;
}

In general, it is desirable that after an object throws an exception it still be in a well-defined, consistent, usable state as it was before the invocation of a method even if the exception is thrown in the middle of the method. This failure-atomicity property of the method is especially important for checked exceptions, from which the caller is expected to recover. There are several ways to achieve that property:

  • Design immutable objects.
  • Check parameters for validity before performing an operation (see the second implementation above).
  • Order the computation so that any part that may fail takes place before any part that may modify the object.
  • Perform the operation on a temporary copy of the object and replace the contents of the object with those of the copy if and once the operation is complete.
  • Write recovery/rollback code to restore the object to its initial state if a failure occurs.

Note that achieving failure atomicity may significantly increase cost and complexity of the method; hence, it is not always desirable. In addition, in some situations it is not always achievable. For example, when  ConcurrentModificationException  occurs it would be wrong to assume that the object is still consistent and usable; this kind of exceptions and  AssertionError  are unrecoverable and you should not attempt to preserve failure atomicity.

Leave a Reply

%d bloggers like this: