Exceptions

Item69: Use exceptions only for exceptional conditions

Exceptions should only be used for exceptional conditions, instead of determining whether a normal condition has been met or a loop should be terminated. There are three things wrong with this reasoning:

  • There is little incentive for JVM implementers to make exceptions as fast as explicit tests.
  • Placing code inside a try-catch block inhibits certain optimizations that JVM implementations might otherwise perform.
  • The standard idiom for looping through an array doesn't necessarily result in redundant checks. Many JVM implementations optimize them away.

The moral of this story is simple: Exceptions are, as their name implies, to be used only for exceptional conditions; they should never be used for ordinary control flow. This principle also has implications for API design. A well-designed API must not force its clients to use exceptions for ordinary control flow.

Item70: Use checked exceptions for recoverable conditions and runtime exceptions for programming errors

Java provides three kinds of throwables: checked exceptions, runtime exceptions, and errors. The cardinal rule in deciding whether to use a checked or an unchecked exception is this: use checked exceptions for conditions from which the caller can reasonably be expected to recover.

Use runtime exceptions to indicate programming errors. The great majority of runtime exceptions indicate precondition violations.

If it isn't clear whether recovery is possible, you're probably better off using an unchecked exception。

Errors are reserved for use by the JVM to indicate resource deficiencies, invariant failures, or other conditions that make it impossible to continue execution. Given the almost universal acceptance of this convention, it's best not to implement any new Error subclasses. Therefore, all of the unchecked throwables you implement should subclass RuntimeException.

To help to recover from the exceptional condition, the exception should provide methods that furnish information.

Item71: Avoid unnecessary use of checked exceptions

With checked exceptions, unlike return codes and unchecked exceptions, they force programmers to deal with problems, enhancing reliability. This places a burden on the user of the API, and methods throwing checked exceptions can't be used directly in streams.

The easiest way to eliminate a checked exception is to return an optional of the desired result type. But this method cannot provide any additional information as exceptions could.

We can also turn a checked exception into an unchecked exception by breaking the method that throws the exception into two methods, the first of which returns a boolean indicating whether the exception would be thrown.

if (obj.actionPermitted(args)) {
	obj.action(args);
} else {
	... // Handle exceptional condition
}

if an object is to be accessed concurrently without external synchronization or it is subject to externally induced state transitions, this refactoring is inappropriate because the object's state may change between the calls to actionPermitted and action.

Item72: Favor the use of standard exceptions

Exceptions are no exception to the rule that code reuse is a good thing. The Java libraries provide a set of exceptions that covers most of the exception-throwing needs of most APIs.

  • It makes your API easier to learn and use because it matches the established conventions that programmers are already familiar with.
  • Programs using your API are easier to read because they aren't cluttered with unfamiliar exceptions.
  • Fewer exception classes means a smaller memory footprint and less time spent loading classes.

This table summarizes the most commonly reused exceptions:

Exception Occasion for Use
IllegalArgumentException Non-null parameter value is inappropriate
IllegalStateException Object state is inappropriate for method invocation
NullPointerException Parameter value is null where prohibited
IndexOutOfBoundsException Index parameter value is out of range
ConcurrentModificationException Concurrent modification of an object has been detected where it is prohibited
UnsupportedOperationException Object does not support method

There are other exceptions could be appropriate to use: It would be appropriate to reuse ArithmeticException and NumberFormatException if you were implementing arithmetic objects such as complex numbers or rational numbers.

Sometimes it could be construed as an IllegalArgumentException or an IllegalStateException. Under these circumstances, the rule is to throw IllegalStateException if no argument values would have worked, otherwise throw IllegalArgumentException.

Item73: Throw exceptions appropriate to the abstraction

Higher layers should catch lower-level exceptions and, in their place, throw exceptions that can be explained in terms of the higher-level abstraction. This idiom is known as exception translation:

try {
	... // Use lower-level abstraction to do our bidding
} catch (LowerLevelException e) {
	throw new HigherLevelException(...);
}

A special form of exception translation called exception chaining is called for in cases where the lower-level exception might be helpful to someone debugging the problem that caused the higher-level exception.

try {
	... // Use lower-level abstraction to do our bidding
} catch (LowerLevelException cause) {
	throw new HigherLevelException(cause);
}

The low-level exception has been passed as cause to the higher-level exception, and it could be retrieved later by getCause method.

While exception translation is superior to mindless propagation of exceptions from lower layers, it should not be overused. Where possible, the best way to deal with exceptions from lower layers is to avoid them, by ensuring that lower-level methods succeed.

If it is impossible to prevent exceptions from lower layers, the next best thing is to have the higher layer silently work around these exceptions, insulating the caller of the higher-level method from lower-level problems.

Item74: Document all exceptions thrown by each method

Always declare checked exceptions individually, and document precisely the conditions under which each one is thrown using the Javadoc @throws tag. Don't take the shortcut of declaring that a method throws some superclass of multiple exception classes that it can throw.

While the language does not require programmers to declare the unchecked exceptions that a method is capable of throwing, it is wise to document them as carefully as the checked exceptions.

It is particularly important that methods in interfaces document the unchecked exceptions they may throw. This documentation forms a part of the interface's general contract and enables common behavior among multiple implementations of the interface.

Use the Javadoc @throws tag to document each exception that a method can throw, but do not use the throws keyword on unchecked exceptions.

It should be noted that documenting all of the unchecked exceptions that each method can throw is an ideal, not always achievable in the real world.

If an exception is thrown by many methods in a class for the same reason, you can document the exception in the class's documentation comment rather than documenting it individually for each method.

Item75: Include failure-capture information in detail message

It is critically important that the exception's toString method return as much information as possible concerning the cause of the failure. In other words, the detail message of an exception should capture the failure for subsequent analysis.

To capture a failure, the detail message of an exception should contain the values of all parameters and fields that contributed to the exception. But do not include passwords, encryption keys, and the like in detail messages.

While it is critical to include all of the pertinent data in the detail message of an exception, it is generally unimportant to include a lot of prose.

The detail message of an exception should not be confused with a user-level error message, which must be intelligible to end users. Information content is far more important than readability. User-level error messages are often localized, whereas exception detail messages rarely are.

One way to ensure that exceptions contain adequate failure-capture information in their detail messages is to require this information in their constructors instead of a string detail message.

/**
* Constructs an IndexOutOfBoundsException.
*
* @param lowerBound the lowest legal index value
* @param upperBound the highest legal index value plus one
* @param index the actual index value
*/
public IndexOutOfBoundsException(int lowerBound, int upperBound, int index) {
  // Generate a detail message that captures the failure
  super(String.format("Lower bound: %d, Upper bound: %d, Index: %d",lowerBound, upperBound, index));
  
  // Save failure information for programmatic access
  this.lowerBound = lowerBound;
  this.upperBound = upperBound;
  this.index = index;
}

It may be appropriate for an exception to provide accessor methods for its failure-capture information. It is more important to provide such accessor methods on checked exceptions than unchecked, because the failure-capture information could be useful in recovering from the failure.

Item76: Strive for failure atomicity

Generally speaking, a failed method invocation should leave the object in the state that it was in prior to the invocation. A method with this property is said to be failure-atomic.

There are several ways to achieve this effect:

  • The simplest is to design immutable objects. If an object is immutable, failure atomicity is free.
  • For methods that operate on mutable objects, check parameters for validity before performing the operation
  • Order the computation so that any part that may fail takes place before any part that modifies the object.
  • Perform the operation on a temporary copy of the object and to replace the contents of the object with the temporary copy once the operation is complete.
  • Write recovery code that intercepts a failure that occurs in the midst of an operation, and causes the object to roll back its state to the point before the operation began.

While failure atomicity is generally desirable, it is not always achievable. Even where failure atomicity is possible, it is not always desirable. For some operations, it would significantly increase the cost or complexity.

Item77: Don't ignore exceptions

// Empty catch block ignores exception - Highly suspect!
try {
	...
} catch (SomeException e) {
}

An empty catch block defeats the purpose of exceptions, which is to force you to handle exceptional conditions.

If you choose to ignore an exception, the catch block should contain a comment explaining why it is appropriate to do so, and the variable should be named ignored:

Future<Integer> f = exec.submit(planarMap::chromaticNumber);
int numColors = 4; // Default; guaranteed sufficient for any map
try {
	numColors = f.get(1L, TimeUnit.SECONDS);
} catch (TimeoutException | ExecutionException ignored) {
	// Use default: minimal coloring is desirable, not required
}

The advice in this item applies equally to checked and unchecked exceptions