Although java constructs used in exception handling are well known to almost everybody that has some java experience, I discover each day that a lot of people know too little about
how java exceptions should be used.
Instead of using them as allies in producing more elegant and robust code, I see that a lot of people treat exception like an unnecessary evil that comes along with the java modules they use. They treat this
evil in no time: a simple
catch is enough in almost any situation!
Useless to say that interacting with Java's exception handling mechanism in a
catch the evil as quickly as possible manner usually leads to long hours (days, weeks) of debugging, frustration and in general to
not so elegant solutions to problems.
So, if you intend to build your super-elegant java module, reduce the
if statements in it, make it more readable, or simply want to see typical mistakes performed with java exceptions, you might have a look at the following exception
crimes and
practices. As their names suggest, the exception handling behaviors are scaled from big mistakes to reasonable approaches:
biggestCrime() -> bigCrime() -> ...
-> towardsExceptionHandling() -> basicExceptionHandling()
There are situations when
biggestCrimes are justified, therefore the names only apply to 99.99% of situations!
1. Biggest Crime
The rationale: Developers are in a hurry, they must deal quickly with the evil exception to have more time for the important matters.
The code: public void biggestCrime(){
try {
complexCodeBlockThrowingSeveralExceptions();
} catch (Exception e) {
//no time for this now, I'll deal with it later
}
}
The consequences:
Developer usually forgets to come back to the block of code. As a result, sometime later when a exception will occur, the system won't crash in the obvious place or form, but usually in an unrelated part like one the following :
- when reading some strange values from the database
- when displaying some info on the screen
- when saving the project, etc.
Useless to say that fixing a bug like this could take hours and sometimes days or weeks.
2. Biggest Crime Variation
The rationale: Developers overestimate their knowledge about the raised exceptions or just blindly trust the specs they are implementing. The "I know this won't happen in our case" scenario.
The code: public void biggestCrimeVariation(){
try {
complexCodeBlockThrowingSeveralExceptions();
} catch (Exception e) {
//I guess that this won't happen to me
}
}
The consequences:
Only the comment differs from the previous case: same consequences!
3. Big Crime
The rationale: Like in the scenarios above developers overestimate or are in a hurry. They do have still some doubts about their behavior.
The code:
Logger log = Logger.getLogger(ExceptionCrimes.class);
...
public void bigCrime(){
try {
complexCodeBlockThrowingSeveralExceptions();
} catch (Exception e) {
//I know that this won't happen,
//but I log something "just in case"
log.error(e.getMessage());
}
}
The consequences:
A big improvement as opposed to the previous cases. When an exception will be raised, if you have
eagle eye you will see a small line in the big fat log file saying that something might be wrong somewhere in the code.
The log line will be prefixed with the precious
ERROR keyword, the message itself will be usually completely useless: "null pointer exception", "file not found exception", etc.
4. Big Crime Lazy Variations
The rationale: This scenario is very similar with the one above. There are still two differences: developers are lazy (they like CTRL + F1 - quick fix - editor options like "Surround with try catch"), developers are not lazy (they are never lazy for copy-paste operations!)The code:
Logger log = Logger.getLogger(ExceptionCrimes.class);
...
public void bigCrimeLazyVariation(){
try {
complexCodeBlockThrowingSeveralExceptions();
}
catch (IllegalArgumentException e) {
//I know that this won't happen,
//but I log something "just in case"
log.error(e.getMessage());
}
catch (FileNotFoundException e) {
//I know that this won't happen,
//but I log something "just in case"
log.error(e.getMessage());
}
catch (NullPointerException e) {
//I know that this won't happen,
//but I log something "just in case"
log.error(e.getMessage());
}
}
The consequences:
Same behavior like for the previous case: We have the same code, but in a less readable and maintainable form!
5. Regular Crime
The rationale: This scenario is very similar with the ones above. The important difference is the stack trace: the developers know what it is, that it saves time when maintaining code and how to include it in the log!The code:
Logger log = Logger.getLogger(ExceptionCrimes.class);
...
public void majorCrime(){
try {
complexCodeBlockThrowingSeveralExceptions();
} catch (Exception e) {
//I know that this won't happen,
//but I log everything "just in case"
log.error(e.getMessage(), e);
}
}
The consequences:
Massive improvement from the maintenance point of view. When the system crashes, the log file will record a long (and easily identifiable) stack trace prefixed by an useless error message.
The major improvement consists in the fact that the log signals the system's failure when the failure occurs. (Fails fast)
6. Still A Crime
The rationale: Developers still don't do exception handling, but they don't like useless error messages either!
The code:
Logger log = Logger.getLogger(ExceptionCrimes.class);
...
public void stillACrime(String param){
try {
complexCodeBlockThrowingSeveralExceptions();
} catch (Exception e) {
//I know that this won't happen,
// but if something will happen, I'll add a better message!
log.error("Could not access my desired module", e);
}
}
The consequences:
Massive improvement from the maintenance point of view. When something wrong happens in the system, one will know what happened from the system's point of view!
7. Minor Crime
The rationale: Developers still don't do exception handling, but they know information needed for effective exception handling!
The code: Logger log = Logger.getLogger(ExceptionCrimes.class);
...
public void minorCrime(String param){
try {
complexCodeBlockThrowingSeveralExceptions();
} catch (Exception e) {
//I know that this won't happen,
// but if something will happen, I'll log all that I can!
String err = "Could not access my desired module. Used param: "+param;
log.error(err, e);
}
}
The consequences:
Massive improvement from the maintenance point of view. When something wrong happens in the system, one will know what happened from the system's point of view and in what context without looking at the code!
8. Exception Handling Crime
The rationale: Developers do know exception handling mechanisms, but they do not know the stack trace!
The code: public void exceptionHandlingCrime(String param){
try {
complexCodeBlockThrowingSeveralExceptions();
} catch (Exception e) {
//this is a known problem I will deal with it in the upper layers!
throw new MyModuleAccessException("Could not access my module's configuration file");
}
}
The consequences:
Logging is not performed everywhere (in the lower and upper layers), but only in the upper layers.
The meaningless "null pointer exception" has become "cannot access my module" simply looking at the exception's name.
Some detail information is given - a configuration file is missing.
Still: hard to identify the exception in the log file, no context at all.
9. Towards Exception Handling
The rationale: Developers do know exception handling, do know the stack trace, but don't give enough context.
The code: public void towardsExceptionHandling(String param){
try {
complexCodeBlockThrowingSeveralExceptions();
} catch (Exception e) {
//this is a known problem I will deal with it in the upper layers!
throw new MyModuleAccessException("Could not access my module's configuration file", e);
}
}
The consequences:
In addition to the previous case, we have the stack trace, but we still have to look at the code to see the exact context.
10. Basic Exception Handling
The rationale: Developers do know exception handling. They do know the basics of using custom exceptions and their benefits.
The code: public void basicExceptionHandling(String param){
try {
complexCodeBlockThrowingSeveralExceptions();
} catch (Exception e) {
//this is a known problem I will deal with it in the upper layers!
throw new MyModuleAccessException("Could not access my module's configuration file on physical path: "+param, e);
}
}
The consequences:
When something wrong happens, we know and we know exactly what happened!
Epilogue
The
consequences for the ten scenarios above should not be taken to strictly. Sometimes, usually from performance reasons, all scenarios above can be justified (input-output operations take time, evaluating expressions when logging is time consuming and potential source of programming errors, etc.).
But then again, premature optimization is the root of all evil: it's easier to optimize well organized, working code, than to follow, maintain, test, prematurely optimized code!
As a summary, the following rules of thumb helped me a lot when dealing with exceptions:
- re-throw them if you don't know the fix
- log them with stack trace and detailed context if they are unexpected
- add more system related information when re-throwing (say more)