Exceptions should be preferred over error codes as returning error codes will force you to have an “if” statement to check for error code, which may be confusing to some readers and there is also a chance that the caller may forget to check for error codes. If you use error codes, you will also need to have some class that defines all error codes.
Exceptions can be handled easily using a try-catch block in Java. In a try catch block, it is preferred to move the contents of try and catch to separate methods as it will separate error processing with normal processing into separate methods giving better clarity.
When using try-catch for error handling, there should not be any other code before or after the try-catch in a method, as error handling is one thing and hence should reside in its own method to follow single responsibility principle.
According to the book Clean Code, it is a good practice to start with try-catch-finally when you are writing code that could throw exceptions, as this helps you define what user of that code should expect, no matter what goes wrong with the code that is executed in the try.
We can follow a test driven approach by writing the unit tests first to check exceptions. Initially the code will not throw exception and our tests will fail. We can then write the implementation that will lead to the exception during the invalid usage scenario and then our tests will pass.
Prefer unchecked exceptions over checked exceptions, unless it is really needed. If you introduce an exception in a class low in the hierarchy, then all its callers’ signature needs to be modified or one of them needs to handle it using a try-catch block, all these modified classes needs to be compiled again. This means that higher level methods must know about the internals of the lower level methods and hence checked exceptions break encapsulation.
Exceptions that we throw should provide enough information about context like source and location of error, operation that failed type of failure, so that enough information may be logged for further analysis.
We should wrap lower layer exceptions and pass these wrappers to higher levels. This wrapping, also may be called as exception translation, reduces the dependency on lower level APIs. This is a very useful technique especially when you wrap third party APIs, as higher level methods are now not tied to a particular vendor APIs.
The book Clean Code says about passing or retuning null as: Returning null from a method is bad, but passing null is worse. Passing null may result in NullPointerException, which again you will need to handle at some point, if you need to end the program gracefully. Checkout the use of Java 8 Optional.
Instead of returning null, or some odd value, Fowler suggests to return a Special Case that has the same interface as what the caller expects. This is more polymorphic and you can just rely on the type checking features of your programming language alone, without the need of extra checks for null. Instead of using a null reference to convey absence of an object (for instance, a non-existent customer), one uses an object which implements the expected interface, but whose method body is empty. The advantage of this approach over a working default implementation is that a Null Object is very predictable and has no side effects: it does nothing. Passing an object without any implementation instead of null is also called Null Object design pattern.
For further best practices for using and writing exceptions in Java, please refer http://javajee.com/best-practices-for-using-and-writing-exceptions-in-java.
This note is primarily based on the book “Clean Code” by “Robert C. Martin”.