Exceptions were introduced into the Java language to separate the functional code from error-handling code. They allow for clear propagation path of a specific error.
There are two types of exceptions:
checked exceptions - compiler enforced exceptions that are instances of the Exception class or one of its subclasses and the
unchecked exceptions, runtime exceptions like
RuntimeException and its subclasses and
Error and its subclasses.
A compiler for the Java programming language checks, at compile time,
that a program contains handlers for
checked exceptions.
Many times in my carrier as a software developer I had to read, debug, review code from some other developers. Many times I've seen silenced exceptions like:
try {
someFunction(); // may throw an exception
} catch (Exception e) {
// do nothing}
I believe this type of code is Evil. Something happens in the code and the developer decides that the best way to go is to do nothing. If this code influences other code there will be no way to know what really happened with the code. If the code does not influence other code it is still no way to know that some functionality was not executed. The least a programmer can do in such situation is to write some minimal information to the log file.
Imagine that you are not able to debug a code that runs in production environment, the only means to investigate a problem is the log files. Every time a checked exception is correctly handled your job will be easier to investigate potential issues.
I have next a number of advices to follow when dealing with exceptions:
- NEVER SILENCE AN EXCEPTION like in my above example.
- Only throw checked exceptions (not derived from RuntimeException),
if the caller has a chance to handle it.
class ApplicatioException extends Exception { // classic checked exception
public ApplicationException (String str) {
super (str);
}
}
...
class Application {
public void doSomeAction () throws ApplicationException {
...
if (bad) {
throw new ApplicationException ();
}
}
public void someOtherAction () {
try {
this.doSomeAction();
} catch (ApplicatioException ex) {
logger.error("doSomeAction failed miserably!"); // log information
}
}
}
- Checked exceptions are an official part of the interface, therefore
do not propagate checked exceptions from one abstraction layer to
another, because usually this would break the lower abstraction. E.g. do
not propagate SQLException to another layer, because SQLExceptions are
an implementation detail, that may change in the future and such changes
should not affect the interfaces and their callers.
class DBUtil{
public static void closeConnection
(Connection conn){
try{
conn.close();
} catch(SQLException ex){
throw new DBUtilException(ex); // propagate exception to the next level
}
}
- Never throw NullPointerException or RuntimeException.
Use either IllegalArgumentException, or NullArgumentEception
(which is a subclass of IllegalArgumentException anyway). If
there isn't a suitable subclass available for representing an exception,
create your own.
class DBUtil{
public static void closeConnection
(Connection conn){
try{
conn.close();
} catch(SQLException ex){
throw new RuntimeException(ex); // never throw an exception like this
}
}
- Only if it is not possible to return special result values cleanly, use
checked exceptions to force the caller to decide the situation. The
caller should deescalate the situation by catching and handling one or
more checked exceptions, e.g. with special result values or by
escalating with an unchecked exception, because the situation is an
error, that can not be handled.
- Exceptions that signal programming errors or system failures usually
cannot be handled/repaired at runtime -> unchecked exception.
- Do NOT throw an exception, if you only suppose the caller of your
code could have a problem with a special result. Try to return a special
result value instead e.g., null, and let the caller decide with a
regular if-else-statement. If the caller really has a problem, HE WILL
throw an exception on his own.
class Example{
public Result exampleAction (){
Result result = null;
... // some result processing
return result; // return the result in any form
}
}
public boolean processResult () throw ResultException {
Result result = exampleAction();
if (result == null) {
return new ResultException (); // caller has a problem here; unexpected result
}
else if (!isValid(result)) {
return fail; // result failure
}
return success;
}
}
- The intention of exception-handling is to separate real
error-handling from the regular part of the code, so don't force the
caller to mix it with unnecessary exceptions.
I believe proper exception handling is a good indicator that a programmer understands the programming language he/she uses and is able to do good job.