This article lays down some of the best practices which you can use during your design or code reviews. I have learned many of these practices from Effective Java by Joshua Bloch.
Exceptions should be only used for exceptional conditions. It should not be used for control flow like catching an exception and ending a loop.
APIs must provide alternatives to exceptions to control the flow. For example, if Iterator class in collections does not have a hasNext() method, we would have to use a while(true) loop and then catch NoSuchElementException to exit the loop.
We can have either state testing method (like hasNext() ) and then call the dependent method (like nextItem()) or return a distinguished value (such as null) when the object is in an inappropriate state.
Though a state testing method is the preferred one, if there is a chance for the state to change between a state testing method call (like hasNext()) and its dependent method (like nextItem()), or if a separate state testing method could duplicate the work of the state dependent method, we can use distinguished return type method.
When using a distinguished return value, we need to make sure that the distinguished value is not a possible correct value that can be returned.
Catching a checked exception and ignoring it is usually a bad idea. Or at the very least, the catch block should have a comment explaining why it is appropriate to ignore.
If there is a chance of recovery, then use checked exception, else use unchecked exception; and if in doubt, it would be better you use unchecked exception. Overuse of checked exception can make an API far less user friendly.
Even though java spec does not require, there is a strong convention that errors are reserved for use by the JVM to indicate failures that make it impossible to continue.
We should not use Throwable instead of checked exceptions as they will simply confuse users.
You should provide methods in your exception classes to get the details of the failure programmatically and should not leave them with no option than to parse the string representation.
Whenever possible use standard exceptions provided by Java or subclass them and add more functionality. This will make the code easier to learn and use.
Higher layers should catch lower layer exceptions and in their place, throw exceptions that can be explained in terms of the higher level abstraction. This is called exception translation. Exception translation should not be overused.
For better debugging we can do exception chaining by passing the lower level exception (the cause) to higher level exception (using a suitable constructor or Throwable.initCause) and then access it later using an accessor (using Throwable.getCause).
Always declare checked exceptions individually and not a common parent class for all the exceptions; don’t ever declare ‘throws Exception’.
Do not declare unchecked exceptions using throws clause.
Document all checked and unchecked exceptions using the javadoc @throws tag and the conditions under which it is thrown. Even though documenting all of the unchecked exceptions is ideal, it is not always achievable in real world.
Detail message of an exception should capture as much information as possible.
Whenever an exception is thrown from a method, we need to make sure that the current object is in the state prior to execution of that method or at least we should document what state the object will be after that exception is thrown. There are various ways to implement this failure atomicity like making the class immutable, checking parameters for validity and throwing exception before performing the operation, write recovery code to roll back or performing the operation on a temporary copy of the object. If failure atomicity is not practical due to cost and complexity, don’t do it, but document it. Errors are unrecoverable and hence we should not even attempt for failure atomicity for them.