http://www.developer.com/

Back to article

Handling Anomalies: Errors and Exceptions


August 6, 2007

Introduction

This is the thirty-seventh installment in a series of articles about fundamental object-oriented (O-O) concepts and related object-oriented technologies. The material presented in these articles is based on material from the second edition of my book, The Object-Oriented Thought Process, 2nd edition. The Object-Oriented Thought Process is intended for anyone who needs to understand basic object-oriented concepts and technologies before jumping directly into the code. Click here to start at the beginning of the series.

In keeping with the code examples used in the previous articles, Java will be the language used to implement the concepts in code. One of the reasons that I like to use Java is because you can download the Java compiler for personal use at the Sun Microsystems Web site http://java.sun.com/. You can download the standard edition, J2SE 5.0, at http://java.sun.com/j2se/1.5.0/download.jsp to compile and execute these applications. I often reference the Java J2SE 5.0 API documentation and I recommend that you explore the Java API further. Code listings are provided for all examples in this article as well as figures and output (when appropriate). See the first article in this series for detailed descriptions for compiling and running all the code examples (http://www.developer.com/design/article.php/3304881).

The code examples in this series are meant to be a hands-on experience. There are many code listings and figures of the output produced from these code examples. Please boot up your computer and run these exercises as you read through the text.

For the past several months, you have been exploring various issues regarding object integrity, security, and performance. In this month's article, you review a concept that I touched on briefly earlier in the series error handling. In this article, you will cover the basic concepts of error handling with the intent of delving much deeper into this topic in future articles.

The Meaning of a Word?

It is important to realize that this article's focus is on error handling and not specifically on exceptions. Although exceptions are the true objects (as with all objects, the Exception class inherits from the Object class), Exceptions are not the only technique for error handling. In fact, the terminology can get a bit tricky. Technically, errors and exceptions are defined as two totally separate entities in the Java programming specification. Both errors and exceptions inherit from a class called Throwable, as seen in Diagram 1.

Diagram 1: The Throwable inheritance tree.

Although you often hear the terms 'catching an exception' or 'handling an error' used quite often, you don't often hear the term 'Throwable' used in many places (although you may well hear someone refer to throwing an exception).

This is why the terminology may get a bit confusing when discussing error handling. Error handling has always been a major part of programming; however, throwing and catching Exceptions are somewhat newer concepts. In any case, what exactly does Throwable mean and how does it relate to Errors and Exceptions?

What Is Throwable?

The definition from the Java API specifications describes the Throwable class as follows:

The Throwable class is the superclass of all errors and exceptions in the Java language. Only objects that are instances of this class (or one of its subclasses) are thrown by the Java Virtual Machine or can be thrown by the Java throw statement. Similarly, only this class or one of its subclasses can be the argument type in a catch clause.

http://java.sun.com/j2se/1.5.0/docs/api/

In my experience, the best way to distinguish between an Error and an Exception is this: An error tends to be an anomaly that is so serious that the application can't continue. This may be something such as a hardware failure or a memory issue that corrupts the execution of an application. In general, it may not be the best idea to catch an Error; in fact, it well might be impossible. When encountering an Error, as you may have surmised, the application will terminate abnormally (in other words, a crash). The Java API describes the Error class in this way:

An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions. The ThreadDeath error, though a "normal" condition, is also a subclass of Error because most applications should not try to catch it.

http://java.sun.com/j2se/1.5.0/docs/api/

The Exception, on the other hand, exists to assist in the flow of an application. Exceptions occur in situations where the operating system identifies an anomaly, yet the application is capable of 'catching' the anomaly—and then presumably handling it appropriately. The definition for the Exception class is as follows:

The class Exception and its subclasses are a form of Throwable that indicates conditions that a reasonable application might want to catch.

http://java.sun.com/j2se/1.5.0/docs/api/

You will explore the important differences between Errors and Exceptions in a later article. However, with this brief description of the Throwable class hierarchy, let me explain the general concept of error handling by using the term anomaly so that you can be inclusive of Errors and Exceptions without the confusion.

Anomaly Handling

It is rare for a class to be written perfectly the first time. In most, if not all, situations, things will go wrong. Any designer who does not plan for problems is courting danger.

Assuming that your code has the ability to detect and trap an anomalous condition, you can handle the anomaly in several different ways: In the book Java Primer Plus, the authors state that there are three basic solutions to handling a problem that is detected in a program: "fix it, ignore the problem by squelching it, or exit the runtime in some graceful manner".

A variation on this theme is presented in the book Object-Oriented Design in Java. The authors expand on this theme by adding the choice of throwing an exception. The options are:

  • Ignore the problem—not a good idea!
  • Check for potential problems and abort the program when you find a problem.
  • Check for potential problems, catch the mistake, and attempt to fix the problem.
  • Throw an exception. (Often, this is the preferred way to handle the situation.)

Let me talk about each of these strategies.

Ignoring the Problem

Simply ignoring a potential problem is a recipe for disaster. To illustrate, consider the situation where you purposely generate an anomaly. In this case, you do a simple divide by zero that will untimely crash the application, as seen in Listing 1.

// Class ErrorHandling
public class ErrorHandling {

   public static void main(String[] args) {

      int a = 0;
      int b = 0;
      int c = 0;

      c = a / b;    // divide by zero

      System.out.println("At the end ");

   }

}

Listing 1: Generating an Error—Ignoring the exception

Understanding the flow of control in these applications is important. As you see in Diagram 2, the application's flow of control is interrupted by the operating system when the divide by zero is encountered.



Click here for a larger image.

Diagram 2: An Object with two References and two Objects (different content)

At this point, the application loses control of the process and is aborted. Control returns to the operating system. You can see this by observing the output in Figure 1.



Click here for a larger image.

Figure 1: Generating an Error—Ignoring the problem

This scenario is not a happy ending for the user. How many times have you been using a software application when the application crashed? Although this used to be a more common occurrence several years ago, it is still an unwelcome situation. And, when a situation such as this does occur, the results can be anywhere from annoying to devastating. When an application has an uncontrolled abort, there is a distinct possibility that data will be lost or corrupted. One of the cardinal rules pertaining to software applications is that an application should avoid an uncontrolled abort (crash) at all costs. If an application continuously crashes, users are very likely to stop using (buying) the product.

If you do make the effort to detect the problem, you might as well figure out how to handle it; if you are going to ignore the problem, why bother detecting it in the first place? The bottom line is that you should not ignore the problem. If you do not handle your errors, the application will eventually terminate ungracefully or continue in a mode that can be considered an unstable (unsafe) state. In the latter case, you might not even know you are getting incorrect results for some period of time—and this can be dangerous.

Checking for Problems and Terminating the Application Gracefully

If you decide to check for potential problems and exit the application when a problem is detected, the application can, at least, display a message indicating that you have a problem and what action is being taken.

Detecting a problem and then gracefully exiting means that the application identifies a problem, cleans up the mess as best as possible, and then decides to terminate the application. This is not a crash. Rather than allowing the operating system to indiscriminately abort the application, the application (via the programmer) remains in control of the process. Then, the programmer simply decides that it is better, and/or safer, to exit the application.

This strategy allows the programmer to code clean-up methods that can perform important functions such as saving or closing files, sending messages, and putting the system in a safe-state. Consider an elevator system. If there is a fire alarm anomaly, you would want all the elevators to either proceed to the first floor, or at least open the doors at the closest floor. If the application simply crashes, there could be people left stranded in the elevators during a fire. This is all well and good; however, you better let the user know what is happening.

If you exit the application without letting the user know, the code may in fact terminate gracefully (meaning no crash); however, the user is left staring at the computer screen, shaking his/her head and wondering what just happened.

Although gracefully exiting the application when an anomaly is detected is a far superior option to ignoring the problem, it is by no means optimal. However, this does allow the system to clean up things and put itself in a more stable state, such as closing files.

// Class ErrorHandling
public class ErrorHandling {

   public static void main(String[] args) {

      int a = 0;
      int b = 0;
      int c = 0;

      if (b != 0) {    // catch the divide by zero
         c = a / b;
      } else {
         System.out.println("Error: Divide by zero");
         System.exit(0);    //abort under control
      }

   }

}

Listing 2: Generating an Error—Ignoring the exception

With the if statement, in the case of the code in Listing 2, you check to make sure that a division by zero never happens. When a division by zero is detected, the code is skipped and an error is printed. Diagram 3 shows the program flow in this situation. Note that the operating system eventually gets control of the process back; however, the application exits while in control and gives control back to the operating system voluntarily.



Click here for a larger image.

Diagram 3: An Object with two References and two Objects (different content)

The output produced when the code in Listing 2 is executed is presented in Figure 2. As you can see, the user is not shown the exception messages coming directly from the operating system. The users view what the programmer decides to show them.



Click here for a larger image.

Figure 2: Generating an Error—Catching the problem and aborting

Checking for Problems and Attempting to Recover

Checking for potential problems, catching the mistake, and attempting to recover is a far superior solution than simply checking for problems and gracefully exiting. In this case, the problem is detected by the code, and the application attempts to fix itself. This works well in certain situations. You could prompt the user to re-enter until an appropriate value is determined. Listing 3 is one solution—but not necessarily the best.

import java.io.*;

// Class ErrorHandling
public class ErrorHandling {

   public static void main(String[] args) throws Exception {

      int a = 0;
      int b = 0;
      int c = 0;

      b = getInput();

      while (b == 0) {    // catch the divide by zero

         System.out.println("Error: Divide by zero. Please reenter.");
         b = getInput();

      }

      c = a / b;
      System.out.println("c = " + c);
   }

   public static int getInput() throws Exception {

      int x;

      BufferedReader stdin =
         new BufferedReader(new InputStreamReader(System.in), 1);

      System.out.print("Please Enter Number:");
      String s1 = stdin.readLine();

      x = Integer.parseInt(s1);    // string to double

      System.out.println("x = " + x);

      return (x);

   }

}

Listing 3: Generating an Error—Catching the problem and handling it.

Note that the code in red is in place to re-prompt the user for valid input. Also presented in red, note that you are ignoring the required exception detection. The anomaly is detected so that no division takes place with a zero denominator. Rather than terminate, the application can continue. In theory, the operating system does not come in to play, as seen in Diagram 4.



Click here for a larger image.

Diagram 4: Generating an Error—Catching the problem and handling it.

In this case, the user is kept informed by an error message indicating what the problem is and then is asked to re-enter a valid value, as seen in Figure 3. It is important to make sure that the messages to the user are specific and clear.



Click here for a larger image.

Figure 3: Generating an Error—Catching the problem and handling it.

Despite the fact that this type of error handling is not necessarily object-oriented in nature, I believe that it has a valid place in OO design. Throwing an exception (discussed in the next section) can be expensive in terms of overhead (you will learn about the cost of exception handling in a later article). Thus, although exceptions are a great design choice, you will still want to consider other error handling techniques, depending on your design and performance needs.

Although this means of error checking is preferable to the previous solutions, it still has a few potentially limiting problems. It is not always easy to determine where a problem first appears. And, it might take a while for the problem to be detected. It is always important to design error handling into the class right from the start.

Throwing an Exception

Most OO languages provide a feature called exceptions. In the most basic sense, exceptions are unexpected events that occur within a system. Exceptions provide a way to detect problems and then handle them. In Java, C#, and C++, exceptions are handled by the keywords catch and throw. This might sound like a baseball game, but the key here is that a specific block of code is written to handle a specific exception. This solves the problem of trying to figure out where the problem started and unwinding the code to the proper point. Here is how the code for a try/catch block looks:

try {

   // Business/program logic

} catch(Exception e) {

   // Code executed when exception occurs

}

If an exception is thrown within the try block, the catch block will handle it. When an exception is thrown while the code in the try block is executing, the following occurs:

  • The execution of the try block is terminated.
  • The catch clauses are checked to determine whether an appropriate catch block for the offending exception was included. (There might be more than one catch clause per try block.)
  • If none of the catch clauses handle the offending exception, it is passed to the next higher-level try block. (If the exception is not caught in the code, the system ultimately catches it and the results are unpredictable.)
  • If a catch clause is matched (the first match encountered), the statements in the catch clause are executed.
  • Execution then resumes with the statement following the try block.

Listing 4 an example of how an exception is caught by using the code from the previous examples:

import java.io.*;

// Class ErrorHandling
public class ErrorHandling {

   public static void main(String[] args) throws Exception {

      int a = 9;
      int b = 0;
      int c = 0;

      b = getInput();

      try {

         c = a / b;

      } catch(Exception e) {

         System.out.println("\n*** Exception Caught");
         System.out.print("*** System Message : ");
         System.out.println(e.getMessage());
         System.out.println("*** Exiting application ...\n");

      }

      System.out.println("c = " + c);
   }

   public static int getInput() throws Exception {

      int x;

      BufferedReader stdin =
         new BufferedReader(new InputStreamReader(System.in), 1);

      System.out.print("Please Enter Number:");
      String s1 = stdin.readLine();

      x = Integer.parseInt(s1);    // string to double

      return (x);

   }

}

Listing 4: Generating an Error—Catching the exception.

Exception Granularity

You can catch exceptions at various levels of granularity. You can catch all exceptions or just check for specific exceptions, such as arithmetic exceptions. If your code does not catch an exception, the Java runtime will—and it won't be happy about it!

In this example, as you have already seen, the division by zero within the try block will cause an arithmetic exception. If an exception is actually generated (thrown) outside a try block, the program would most likely have been terminated. However, because the exception was thrown within a try block, the catch block is checked to see whether the specific exception (in this case, an arithmetic exception) was planned for.

Diagram 5: Generating an Error—Catching the exception.

As seen in Diagram 5, the operating system and the application are, in effect, working together to handle the problems. Although the operating system (or Virtual Machine) does actually generate the exceptions, the application detects them and can handle them as desired. Figure 4 shows what the user sees from the various messages printed after the exception is handled.



Click here for a larger image.

Figure 4: Generating an Error—Catching the exception.

Conclusion

It is almost certain that every system will encounter unforeseen problems. Thus, it is not a good idea to simply ignore potential errors. The developer of a good class (or any code, for that matter) anticipates potential errors and includes code to handle these conditions when they are encountered.

The rule of thumb is that the application should never crash. When an error is encountered, the system should either fix itself and continue, or exit gracefully without losing any data that's important to the user.

It's a good idea to use a combination of the methods described here to make your program as bulletproof for your user as possible.

References

  • www.sun.com
  • http://java.sun.com/j2se/1.5.0/docs/api/
  • Java Primer Plus: Supercharging Web Applications With the Java Programming Language. Tyma, Torol, Downing. ISBN: 157169062X
  • Object-oriented design in Java. Stephen Gilbert; Bill McCarty. ISBN: 1571691340

About the Author

Matt Weisfeld is a faculty member at Cuyahoga Community College (Tri-C) in Cleveland, Ohio. Matt is a member of the Information Technology department, teaching programming languages such as C++, Java, C#, and .NET as well as various web technologies. Prior to joining Tri-C, Matt spent 20 years in the information technology industry, gaining experience in software development, project management, business development, corporate training, and part-time teaching. Matt holds an MS in computer science and an MBA in project management. Besides The Object-Oriented Thought Process, which is now in its second edition, Matt has published two other computer books, and more than a dozen articles in magazines and journals such as Dr. Dobb's Journal, The C/C++ Users Journal, Software Development Magazine, Java Report, and the international journal Project Management. Matt has presented at conferences throughout the United States and Canada.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date