6.3 How to Handle Errors Incorrectly

 < Day Day Up > 



6.3 How to Handle Errors Incorrectly

An important part of implementing components is effectively handling errors that inevitably will occur. Using exceptions to handle program errors at runtime is probably the best way to implement an error-handling strategy. The use of exceptions allows a program to implement a consistent and flexible strategy for dealing with errors and is very useful in keeping generic objects generic. Most books seem to assume that the reader understands error-handling strategies and why strategies such as the try-catch blocks and exceptions in Java are powerful. These books then proceed to explain how to do exception handling in Java. Most students, however, tend to build programs around getting the program to work and then paste in error handling as an afterthought when problems occur. In larger, real-world systems, this is a recipe for inconsistent and improper error handling. In these systems, inconsistent and even improper handling of errors can result in failure of an otherwise well-designed and well-written program. Correct error handling cannot be added to a program after it is completed; it must be built into a program from its inception to ensure that it is implemented in a coherent and consistent manner; when it is done in an ad hoc fashion, it will probably never be correct.

To help the reader understand why exceptions are included in the Java language to handle errors correctly and why correct error handling is important, this chapter first looks at two bad strategies for handling errors. This should help the reader understand the power of exception handling and provide a basis for understanding the Java implementation of exceptions. The chapter then explains how try-catch blocks can be used in Java to handle exceptions properly and provides rules for how to use exceptions.

6.3.1 Handle an Error When It Occurs

The first error-handling strategy programmers often employ is dealing with the error when it occurs. In this strategy, after the programmer has written the program, a problem is found during testing and debugging (or even later, when the program is in production). The programmer corrects this problem by adding logic to the program for handling that error, often without considering the overall impact of that change. For example, in Program 5.4c, a ClassCastException can occur if an object stored in the table and passed as a parameter to the gt method is not a Person object. A programmer who encounters this problem sees that the easiest way to fix this problem is to have the method print out an error message and return false, reasoning that the user has been notified of the problem and if the object is not of the right type the method should return false as it did not work correctly. This modification to the program is shown in Exhibit 1 (Program6.1).

Exhibit 1: Program6.1: Modification to the Person Class to Handle ClassCastException

start example

 public class Person implements PrintableSortable {   private String name;   public Person1(String name){     this.name = new String(name);   }   public void print(){     System.out.println("My Name is "+ name);   }   public boolean gt(Sortable p) {     //We check here to make sure that the object     //passed in is indeed of the correct type.     if (! (p instanceof Person)) {       System.out.println("Invalid object type; must be a         Person.");     return false;     }     else if (name.compareTo(((Person1)p).name) > 0)       return true;     else       return false;   }   public boolean eq(Sortable P) {     return true;   } } 

end example

Now, when an object of incorrect type is sent to the method, the program will give an error message to the programmer that says that an object of an incorrect type was used. This is an example of a "handle it when it occurs" strategy. A problem is identified (an object of the incorrect type has been sent to the gt method), and the program generates an error message that incompatible object types have been entered into the table. This approach solves the programmer's immediate problem, that the logic used to insert records into the table is flawed, and as long as printing this error message provides a meaningful result (in this case, allowing the programmer identify a logic problem), it is a valid error strategy.

We can identify at least three problems with this strategy. First, this message works fine as long as the only person who can possibly get this error is the programmer who wrote the program, as the message that is produced is understandable only to the programmer working on debugging the program. However, the purpose of a program is not to allow a programmer to write and debug code; rather, it is to solve a problem for an end user. If a message does not make sense to users or help them decide what actions to take in response to the message, the system has failed. This is particularly a problem with generic objects such as the SortedPrintTable. The programmer who implements a generic object does not know anything about the application using the generic object; therefore, it is nearly impossible to make informed decisions about how to handle an exception in a generic object. The exception can almost never be handled when it occurs and should be reported back to the application that called the method.

Second, the SortedPrintTable class that is calling this method in the Person class is meant to be generic, and it can be used in any number of programs, which is why so much care was taken to design the SortedPrintTable class around interfaces. But, writing the message to System.out implies that the Person class is only to be used with programs using a terminal-based interface, which has broken the generic nature of the object. If the SortedPrintTable is now to be used by a program that will use a GUI or Web-based interface, the program will not even show the user that an error has occurred. For a Web-based program, the error will likely appear on the operator's console, with the operator unable to identify the objects or program generating the error. Meanwhile, the user, who does not receive any indication that the program has any problems, assumes that the data is being correctly stored in the table. This type of problem is all too common in real-world server programs, even supposedly robust programs developed by experienced programmers.

Finally, handling an error in this way is not only unhelpful, it is wrong. Because the compare could not work, and the gt method could not make a valid decision as to the rank of the object, it simply returns a value of false. This might seem a reasonable thing to do in the context of the Person object, particularly to a programmer attempting to quickly debug the program, but this false return value is interpreted by the table as meaning that the new object is smaller than the previous object, and, rather than the object not being inserted, it is simply inserted at the beginning of the table. This error handling is completely inappropriate for the program but is typical of "handle it when it occurs" strategies for error handling.

Once a "handle it when it occurs" strategy for handling errors has been initiated, the problems begin to multiply. Because the fix for the class cast problem incorrectly inserts the data at the beginning of the table, the programmer may decide to fix the SortedPrintTable so that it will not add a record of the wrong type. To do this, the add method is modified as in Exhibit 2 (Program6.2) so that if the gt method returns false the object in the psArray is checked to see if it is a Person object. It is inserted into the psArray only if it is a Person object. Now the program is correct for a Person object, but the generic SortedPrintTable is no longer generic, as it will only work for a Person object. Other programs that use it will now have to modify this generic table so that it works with their data types, and the SortedPrintTable is sliding down a slippery slope to becoming completely tied to systems that implement it, each implementation having a lot of code specifically for each program that implements it. Because it now has to be modified for each new data type, all systems that use it must be retested when changes are made; otherwise, currently working systems could be broken when the new changes are implemented. In short, the advantages of being generic are completely destroyed, and the SortedPrintTable is well on its way to becoming a mess. By the time this is caught by a lead programmer or architect it is possible that the changes will have gone so deep and become so embedded that the component will never be able to be fixed. Anyone who thinks this scenario is farfetched probably has not worked long enough as a programmer to see systems that have been modified by literally dozens of programmers over many years.

Exhibit 2: Program6.2: Modification of SortedPrintTable to Stop Invalid Person Objects from Being Stored in a Person Table

start example

 public class SortedPrintTable {   private PrintableSortable psArray[];   private int currentSize;   public SortedPrintTable(int size) {     psArray = new PrintableSortable[size];     currentSize = 0;   }   //Method:  delete   //Purpose:   public void delete(PrintableSortable p) {   }   public void add(PrintableSortable p) {     //To handle an empty table     if (currentSize = = 0)     {       psArray[0] = p;       currentSize = 1;       return;     }     //To handle a full table     if (currentSize = = psArray.length) {       return;     }     //Start from the bottom of the table, and move items     //out of the way until we find the place to insert.     for (int i = currentSize-1 ; i > = 0; — i) {       if (psArray[i].gt(p)) {        psArray[i+1] = psArray[i];        //Special Case to handle insertion at the start        //of the table.        if (i = = 0)         psArray[0] = p;        }        else {          //Check to see if we have the right object type.          //If the object is of the wrong type, the gt method          //returns false. If this false is because the          //object is the wrong type, we should just return,          //as the gt method already printed out an error.          if (p instanceof Person1)           return;          //We have the right spot, so insert and stop the          //loop.          psArray[i+1] = p;          break;        }     }     currentSize = currentSize + 1;    }    public void printTable(){      for (int i = 0; i < currentSize; ++i)        psArray[i].print();    } } 

end example

This example shows that to be effective error handling must be built into the initial design of the system. Poor error handling can destroy even a well-designed program. The next section looks at another, widely used error-handling method using return values.

6.3.2 Use Return Values

Another poor error-handling strategy uses method return values to signal whether or not the method executed correctly. Adopting this error-handling strategy is not limited to students; in fact, some languages, such as C, rely on this method of handling errors. For example, Exhibit 3 (Program6.3) shows how a typical C program might handle opening and reading from a file. In this program, the user opens a file and gets a file handle. If the value is non-negative, the file is successfully opened. The program then starts reading records from the file until the program reaches the end of file and prints each record out to the terminal.

Exhibit 3: Program6.3: An Example of Error Handling in a Simple C Program

start example

 #include <stdio.h> main() {   FILE *fp;   char buf[80];   fp = fopen("temp.dat," "r");   while(fgets(buf, 80,fp))   {     printf("%s\n," buf);   } } 

end example

A number of problems with this strategy to handle errors are apparent from this program. Generally, data values should not have overloaded meanings. For example, in Exhibit 3 (Program6.3), the return from the open can either be a file handle or a flag indicating whether or not the file was opened correctly. This is often problematic. At best, delineating valid and invalid data based on the value of the field can be difficult; at worst, it can affect the meaning of the program. For example, the gt method of the Person class could be changed so that a return value of 0 means that the object is greater, 1 means the object was equal or less, and -1 means that the comparison was invalid. However, having the method return an int makes the function less intuitive and more difficult to use than the simple return of a boolean value.

Nothing in the strategy forces the programmer to handle the case of a file not being opened correctly. In fact, in Exhibit 3 (Program6.3), an improperly opened file will cause an error when an attempt is made to use the file with the fgets function call. This error will simply shut the program down with a cryptic message, "bus error — core dumped." C programmers argue that a good C program will check all conditions where a program can fail, but errors such as this still occur frequently in C programs, indicating that many C programmers are not checking these problems. The truth is that no management mandate, programming standards document, or code walk-through can ensure that a program will check all possible error conditions or that it will even check the obvious ones a programmer knows can and will happen. Unless the handling of errors is automated and enforced by a compiler or similar mechanism, many, if not most, errors will go unchecked.

Similar to the problem just discussed is the situation when, even if errors are checked, a programmer might not check for the right errors. Consider the loop fgets call in Exhibit 3 (Program6.3). Here, the programmer is relying on the fact that, when the fgets call returns a null pointer, the end of file (EOF) has been reached, terminating the loop; however, any error-reading data can cause the fgets to return a null. In order to find the actual meaning of the error, the error indicator for the file must be checked. This is often not done; in fact many C/C++ programmers do not even know how to check this value.

Even if errors are properly checked for, the impact of those errors might be such that the error could propagate up the call stack to the invoking method, possibly all the way to the main method for the program. This means that all methods must check the return values from all the functions to ensure that the functions are working correctly. Making sure return values are always set and checked requires a disciplined approach to implementing error handling that, despite the best of intentions, is often not implemented.

Often a number of errors can be generated that will all cause the same behavior. For example, in a program that opens many files, if any one file cannot be opened, then the error processing might print out an error message and stop the program. Ideally, this should be achievable in a single error-handling block; however, if return values are used, the return value must be checked for every method call that opens a file. This forces the programmer to insert similar code around every file opened to check for a problem. This is referred to as micromanaging an error and ideally should be avoided.

Because of the drawbacks of handling errors where they occur and return codes, Java implements error handling using a "try-catch" mechanism, similar to the "try-catch" mechanisms in C++ or Exception blocks in Ada. However, unlike C++ (which, unfortunately, was built on top of C and has a mixed mode for exception handling of try-catch and return values), the exception handling in Java is consistently built around using these try-catch blocks. And, unlike Ada and C++, Java implements checked exceptions that can validate that a program does in fact handle errors that are likely to occur. The rest of this chapter explains how exception handling is implemented in Java.



 < Day Day Up > 



Creating Components. Object Oriented, Concurrent, and Distributed Computing in Java
The .NET Developers Guide to Directory Services Programming
ISBN: 849314992
EAN: 2147483647
Year: 2003
Pages: 162

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net