Intelligent Exception Handling
OL
The Problem at Hand
Regardless of the capability of the developer or the attention given to coding, exceptions are going to occur. By design in the Java programming language exceptions exist to allow for the resolution of problems or cases where the normal flow of the application does not support the condition at hand. The problem occurs when the handling of the exceptional condition is haphazard or undisciplined. If an exceptional condition is considered as an operational stream, an exception stream that doesn’t lead to a resolution of the condition represents a black hole into which operation will be diverted but never return resulting in lost data or potentially catastrophic failure.
The most obvious example occurs when control is diverted in to a catch block that doesn’t do anything. In this example, the try/catch block would catch problems that might occur in the attempt to read data from the database but all it does is print the resulting message out to the system console or log. No action is taken in relation to the problems that might occur and no effort is made to report the problem to the application or to the user. So what should be done? That depends on the nature of the problem, the applications ability to deal with it, and how catastrophic the problem is to the system. In the above example, it may be that the user provided a parameter for the query that wouldn’t return any rows of data from the database. This isn’t a critical problem but the user should certainly be notified of the nature of the problem. That is the focus of this paper, what should we as Java developers do when the inevitable, and in some cases hoped for, exception occurs?
What is an exception?
An exception is an event that occurs during the execution of a program that disrupts the normal flow of instructions. Java Tutorials: Exceptions
In general terms an exception is thought of as an error but in point of fact it is a condition that the code cannot deal with by following the normal flow of events. It may well represent an error or it may not. An object that is created and returned from a method call may very well be null if the key value that was passed into the method doesn’t resolve to a persisted object. Such an occurrence does not necessarily constitute an error rather a condition outside normal circumstances. This is a perfectly viable condition that can (and should) occur and be dealt with by the application.
Why do we care about exceptions?
The obvious answer is that we care about exceptions because they represent problems in the application.
The real questions at hand are;
What caused the exception?
Will the calling process be hampered by the exception?
Does the user need to be made aware of the exception?
Can we realistically do anything to correct the problem?
Catch or Specify Requirement
Valid Java code must honor the “Catch or Specify Requirement” meaning that code that might throw certain exceptions must be enclosed by a try/catch block or must specify in the method declaration that it throws the exception(s) potentially generated within the method body. A try must include a handler (catch) for the exception(s) within the try. *NOTE* Code that does not honor the Catch or Specify Requirement will not compile.
Not all exceptions are subject to the Catch or Specify Requirement. Of the three categories of exceptions only one is subject to the requirement.
Checked exceptions are conditions that a well written java application should anticipate and recover from. As an example, a method to create an object from a row of information persisted in a database would very likely take as a parameter a value that the method would use to identify the particular row of information desired. Under normal operations the identifier provided by the user would resolve to a valid row of information but what if the user provides an invalid or non-existent key value? Checked exceptions are subject to the Catch or Specify requirement.
An error is an exceptional condition that is external to the application. The application generally cannot anticipate or recover from this kind of exception. Errors generally originate from the Java Virtual Machine (JVM). When an exception of this type occurs, the ability of the application to resolve the issue is limited generally involving failing (at least) the process in which the error occurred but potentially setting the entire application into a failure state. Errors are not subject to the catch or specify requirement.
The third type of exception is a runtime exception. Runtime exceptions generally result from programming flaws that the application cannot anticipate or recover from and often indicate logic errors or a misused API. It is possible to catch and handle the runtime exception though it generally makes more sense to correct the flaw at its source. Runtime exceptions are not subject to the catch or specify requirement.
Collectively errors and runtime exceptions are referred to as “unchecked exceptions.”
Basic exception handling
Before an exception can be caught, it must be thrown either by the application code, a compiled package or from the Java Runtime Environment. Regardless of where the exception originated it was thrown with a throw statement such as the following example
public Object pop() {
Object obj;
if (size == 0) {
throw new EmptyStackException();
}
obj = objectAt(size - 1);
setObjectAt(size - 1, null);
size--;
return obj;
}
try
{
pop() ;
}
catch(EmptyStackException e)
{
// Here is where we would handle the exception and do something useful
}
Any code that attempted to call pop() would have to catch or specify the EmptyStackException exception or it would not compile.
Now that we’ve thrown an exception, what do we do with it? As we know we have to catch or specify the exception, but which is the appropriate behavior? The answer is that it depends on several variables. At the very least we should almost always log the error and notify the user. There are circumstances where the exception can be resolved within the application logic which might indicate that the user doesn’t need to be notified.
Determination and implementation of the appropriate action in the event of an exception is the responsibility of the developer.
Avoid the urge to use a more general exception. Exception handling is a case where more specific information equates to greater clarity for debugging and for the user of that application. Using more general exceptions such as catching Exception when the real cause is an IOException is very likely to have unintended consequences. Granted that the exception handling code would be simpler with a more general exception but critical debugging information is lost and exceptions are more likely to be mishandled.
The incredible disappearing exception
Exceptions must be dealt with. A catch block that does nothing or prints the stack trace out to the console or log doesn’t resolve the problem.
try
{
// Do something here that would throw an exception
}
catch(SomeException e)
{
e.printStacktrace() ;
}
In the above example, if an exception is thrown in the try block, control is passed into the catch but nothing happens to notify the user that there is a problem and the method exits normally with no work being done. This is referred to as “swallowing the exception.” A better solution might be to log the exception and re-throw the exception to a higher level to be reported to the user.
Exceptions should be logged in ARF using the provided mil.arf.webcore.util.Logger utility API. The convention for printing the Exception to the standard out writer using System.out.println() or Exception.printStacktrace() is not acceptable. Exceptions should be logged using;
public static void log(Exception e){…}
An implementation using the log method might look like this.
public void build()
{
try
{
initConnection() ;
prepareStatement("We need a sql statement here");
preparedStatement.setInt(0, widgetID);
resultSet = preparedStatement.executeQuery() ;
if(resultSet != null)
{
metaData = resultSet.getMetaData() ;
while(resultSet.next())
{
for(int i=0; i { put(metaData.getColumnName(i), resultSet.getObject(i)) ; } } } } catch(SQLException sqle) { Logger.log(Exception e) ; } finally { closeResources() ; } } Documenting exceptions JavaDoc provides a tag for the basic documenting of which exceptions are thrown by a method. The @throws tag provides this basic functionality but does not provide a sufficient level of information without an explanation of the exception and the conditions under which it is thrown. See the following example for the recommended JavaDoc implementation of @throws. /** * Returns the next available sequence. * Note: Connection IS CLOSED upon completion of this method. * @param connection * @return the next available sequence as a String * @throws SQLException if the sql call fails for any reason */ ARF specific exceptions ARF provides exceptions for the processing of actions, commands and content. These exceptions should be used in preference to more generalized exceptions as the arf engine is designed to respond in a predictable manner to these exceptions by providing available handlers relieving the developer of the need to create handlers of their own. Commands should throw CommandException in the event of most checked and unchecked exceptions. ContentGenerators should throw ContentException and SQLProcessors should throw SQLExceptions. The developer can throw the appropriate ARF exception with specific information as the message and the caught exception as the root cause, which brings us to the topic of chained exceptions. “An application often responds to an exception by throwing another exception. In effect, the first exception causes the second exception” (“the Java Tutorials: Essential Classes: Exceptions (Chained Exceptions)”) This is referred to as chaining exceptions. Chaining exceptions provides a greater degree of clarity for debugging purposes and the full stack trace should be logged at some point. However, the full depth of the exception chain need not be reported to the user in most cases. What it all means When handling exceptions it is important to ask if the caller will be able to handle the exception and if they need to be notified when an exceptional case occurs. Work with as specific an exception as possible. Avoid the urge to generalize. Do NOT use empty catch blocks Do not simply print the stack trace (exception.printStackTrace()) Use the provided logging API (mil.arf.webcore.util.Logger) Use (and understand) the provided specific ARF exceptions Make intelligent exception choices that relate to the code at hand Document (JavaDoc) the conditions and circumstances of the exceptions that will be thrown Remember that it is the responsibility of the developer to provide intelligent and capable exception handling to developers calling the code that we write and to provide useful and informative exception reporting to the users of the applications that we build. Appendix A: Examples of Bad Exception Handling public boolean lookForTokenInCollection(Collection c, String token) { if(token == null) { return false ; } Object elem; String tmpString ; Iterator i = c.iterator() ; while(i.hasNext()) { elem = i.next() ; if(elem instanceof ICriteria) { tmpString = ((ICriteria) elem).getSQLValue() ; if(token.equalsIgnoreCase(tmpString)) { return true ; } else if(elem instanceof String) { System.out.println( "Working with a String in SQLutil::lookForTokenInCollection()") ; if(token.equalsIgnoreCase(((String) elem))) { return true ; } else { /* Got an object type I didn't want, so returning false and throwing Exception with stack trace and message */ System.out.println("Odd object of class <" + elem.getClass().getName() + "> in SQLUtil::lookForTokenInCollection() ----- With value of <" + elem + ">") ; try { throw new Exception() ; } catch(Exception e) { e.printStackTrace() ; } return false ; } } return false ; } } } public Map execute( Map input, Map context ) throws mil.arf.webcore.engine.CommandException { /** * */ Map output = new HashMap(); mil.arf.reportcore.user.WebLIDBUser user = (mil.arf.reportcore.user.WebLIDBUser) input.get( "user" ); String niin = (String) input.get("niin"); ReportParamContainer rpc = (ReportParamContainer) input.get("keyValueContainer"); //String reportDescriptorNameTwo = rpc.getReportDescriptorName(); String reportDescriptorRoot = (String)rpc.getReportDescriptor().getProperties().valueForKey("reportNameChoserValue"); if( null != niin && !niin.trim().equalsIgnoreCase("") ) { Connection con = null; String theSQLString = "SELECT ITEM_CONTROL.SOS FROM ITEM_CONTROL WHERE ITEM_CONTROL.NIIN = '"; theSQLString += niin +"'"; try { con = ConnectionUtil.getUserConnection(user); Statement theStatement = con.createStatement(); ResultSet theRS = theStatement.executeQuery(theSQLString); while (theRS.next()) { String theSOS = theRS.getString(1); if("B14".equalsIgnoreCase( theSOS ) || "A12".equalsIgnoreCase( theSOS ) || "B17".equalsIgnoreCase( theSOS ) || "B16".equalsIgnoreCase( theSOS ) || "B46".equalsIgnoreCase( theSOS ) || "B56".equalsIgnoreCase( theSOS ) || "B64".equalsIgnoreCase( theSOS ) || "AKZ".equalsIgnoreCase( theSOS )) { reportDescriptorRoot += "CCSS"; } // DLA gateway else if( theSOS.indexOf("S9") >= 0 ) { reportDescriptorRoot += "DLA"; } else { // todo This is wrong, there will be cases where a NIIN SOS isnt part of the mess and some other action will need to be taken // todo ---someone fix it System.out.println( "Something Broke in GatewayLookup. This is a unexpected SOS we need to deal with... <" + theSOS + "> "); // not a gateway type niin but make it do ccss to to ensure a report descriptor // and although report errors the resultant message matches client reportDescriptorRoot += "CCSS"; } } theRS.close(); theStatement.close(); } catch(Exception w) { } finally { try { con.close();}catch (Exception e){} } } output.put( "reportDescriptorName", reportDescriptorRoot); return output; }