Home | pfodApps/pfodDevices | WebStringTemplates | Java/J2EE | Unix | Torches | Superannuation | | About Us
 

Forward Logo (image)      

Java GUI Programming Tips and Guidelines
Error Recovery

by Matthew Ford
© Forward Computing and Control Pty. Ltd. NSW Australia
All rights reserved.

Error Recovery - Introduction

Error recovery consists of :-
a)   Catching the errors
b)   Restoring the application to some stable state from which the user can continue.

When programming a user interface you should spend 50% (or more) of your time on error recovery. This should include explicit and informative error messages, the suppression of non-critical errors, and the recovery from all errors.

Non-Critical versus Critical Errors

Non-critical errors are ones the user can safely ignore. Critical errors require you to notify the user, either because they corrupt basic data or the program has not be able to perform a user request. For both non-critical and critical errors, you should take steps to recover from the error by restoring the application to some stable state and allow the user to continue.

Non-Critical Errors

Non-Critical Errors are ones that can be safely ignored (as far as the user in concerned) and should just be logged in an error log file. For example:-
If an error occurs when saving the cursor position on application exit, don't prompt the user with an error message, just log it and continue.
If an error occurs on reloading the cursor position on application start-up (probably due to the error above), don't prompt the user, just log the error and continue.

Not having the cursor in the last position may be a slight annoyance to the user but it does not stop them from doing useful work. It is not worthy of notifying the user every time they load a file. Of course you need to exercise some judgement about what is a non-critical error.

Critical Errors

Critical Errors should be notified to the user but almost never require aborting the application

This is because modern applications provide the user with a number of functions, a failure of one of the functions should not require the entire application to shut down. Even an OutOfMemoryError is not fatal. For example JEdit recovers if you run out of memory when trying to open a large file. Parallel also recovers from out of memory errors.

If the error occurs as a result of a user request, such as an undo/redo request, politeness requires that you inform the user that the application could not complete the request, so this is a critical error.

Checked and Unchecked Exceptions.

The standard method, in Java, of notifying the calling method that an error has occurred is by throwing a Throwable or one of its sub-classes (hereafter referred to a an exception with a small 'e').

There are two basic types of exceptions[i] in Java , checked and unchecked exceptions. Throwables that are checked must be declared in a throws clause of any method that could throw them. Unchecked exceptions need not be declared. The Throwables class has two sub-classes, java.lang.Error and java.lang.Exception. All java.lang.Errors are unchecked and need not be declared in a throws clause. Some java.lang.Exceptions are checked e.g. IOException while some are unchecked e.g. IllegalArgumentException.

In the earlier versions of Java (prior to V1.4) there was a clear distinction between checked and unchecked exceptions. Checked exceptions were thrown when some error outside the program's control occurred, such as a file error, while unchecked exceptions where thrown due to some program error, such as an illegal argument. However with the introduction of the chained exception facility in Java V1.4 this general rule disappeared. One of the reasons given in the java.lang.Throwable documentation

".. is that the method that throws it (the exception) must conform to a general-purpose interface that does not permit the method to throw the cause directly. For example, suppose a persistent collection conforms to the Collection interface, and that its persistence is implemented atop java.io. Suppose the internals of the put method can throw an IOException. The implementation can communicate the details of the IOException to its caller while conforming to the Collection interface by wrapping the IOException in an appropriate unchecked exception. (The specification for the persistent collection should indicate that it is capable of throwing such exceptions.)"

That is, the Java class designers found they need to overcome failings in the interface specification. The result of this is that from Java V1.4 onwards you cannot rely on the distinction between unchecked and checked exceptions. This means you now have to catch all exceptions and look at the cause to see how they should be handled. The first four coding rules below cover this in more detail.

Actually I would argue that even without this loss of distinction between checked and unchecked exceptions, you should have been catching all exceptions even prior to Java V1.4.

So the general rule is catch and recover from all exceptions (i.e. Throwables) and only tell the user if you have to.

Note: All exceptions includes java.lang.Error as well as java.lang.Exception. As noted above an OutOfMemoryError is not necessarly fatal and an AssertionError can also be recovered from. You should at least try and log and recover from all java.lang.Error exceptions. If the whole application really fails you are no worse off and often the application won't completely fail when a java.lang.Error is thrown.

For more details on what Sun got wrong with exceptions see Unified Error/Information Messages and Help

Coding Rules for Error Recovery

Rule 1: Catch All Throwables that get to main()
Rule 2: Don't let Throwables escape actionPerformed() methods
Rule 3: Don't let Throwables escape listener methods
Rule 4: Catch all Throwables that get to the Thread run() method
Rule 5: Recover from Throwables
Rule 6: Don't just Catch and Print Throwables
Rule 7: Debugging code should never throw Throwables



Rule 1: Catch All Throwables that get to main()

If an exception ever reaches your application's main() method then your error recovery has failed its job. The best you can do is make sure the user can tell you where the error occurred.

In order to find out what went wrong you must catch the exception in main and write the stack trace to a log file. Printing the stack trace to the terminal window is not sufficient. If your application is not attached to a terminal window then the stack trace will be lost. Even if your application is attached to a terminal window, writing the errors to a well documented log file makes it easier for the user to send you the output so you can fix the error.

So your main() method should always look something like this.

static {    // set up logging for errors
    LogStdStreams.initializeErrorLogging("applicationLog.log",  "Log File for Application "+ new Date(), true, true); 
   // redirect System.out as well as System.err and append to existing log
}                                        

public static void main(String[] args) {
    try{
     ....
      // the real code of your application goes here
     ...
    } catch (Throwable t) {
       System.err.println(StringUtils.toString(t));
    exit(1);
}

The LogStdStreams class allows you to redirect System.err and System.out to a file so that in the catch clause above you can just say
System.err.println(StringUtils.toString(t));
The StringUtils class shows you how to get the stack trace of an exception as a String that you can write to a file.



Rule 2: Don't let Throwables escape actionPerformed() methods

Typical user interaction with your application will be via menu items or button actions. The associated actionPerformed(ActionEvent e) methods are called on the Swing event thread. (java.awt.EventDispatchThread). You should never let actionPerformed(ActionEvent e) methods throw any exception. If the actionPerformed(ActionEvent e) method throws an exception the java.awt.EventDispatchThread.run() method terminates and the Java Virtual Machine tries to print the stack trace to the terminal window (if there is one) and then exits.

Note: The net result is that your program exits, but not through your main() method, so Rule 1 does not help you with errors.

You should catch all exceptions here and give the user a useful message telling them why their requested action was not completed successfully.

Your actionPerformed(ActionEvent e) method should always look something like this.

public void actionPerformed(ActionEvent e) {
   try {
     ...
     // handle action here
     ...
    } catch (Throwable t) {
      // recover from error here
       Application.showErrorMessage(t);
      // if this action is associated with a particular window then use
      // Application.showErrorMessage(window,t);    instead
    }                   
}

Here the Application.showErrorMessage(t1); statement handles the displaying of the message to the user and/or, as appropriate, the logging of the error to the log file.

The ExampleShowErrorMessage.java.txt file contains the outline of a static application wide showErrorMessage() method. Unified Error/Information Messages and Help shows you a method of generating user error messages from exceptions and integrating them into the JavaHelp system.

Running the ExampleShowErrorMessage application displays a dialog box like this

Note that this error message clearly tells the user what has happened and what you intend to do about it and invites the user to continue to use the application. The help button should link to more detailed help. See Why you should not use Dialog boxes to Interrupt the User for further discussion of dialog boxes.



Rule 3: Don't let Throwables escape listener methods

As an extension of rule 2 above, you should catch all exceptions in listener methods. Typical Java library code for calling listeners is

  for (int i = listeners.length - 2; i >= 0; i -= 2) {
    if (listeners[i] == ListDataListener.class) {
      if (e == null) {
         e = new ListDataEvent(source, ListDataEvent.CONTENTS_CHANGED, index0, index1);
      }
      ((ListDataListener)listeners[i+1]).contentsChanged(e);
    }          
  }

If any of the listeners throws an exception none of the following listeners is called. Since there is no guarantee of the order in which listeners are called you cannot be sure which ones are called first.

So you should write all listener methods like this (using contentsChanged() as an example):-

public void contentsChanged(ListDataEvent e) {
  try {
    ..
    // handle event here
    ...
   } catch (Throwable t) {
     // handle errors and recover from errors here
     // and either log error or if important display message to the user.
   }
}   



Rule 4: Catch all Throwables that get to the Thread run() method

User interfaces should make liberal use of threads to keep the user interface responsive. If you don't catch the exceptions thrown in the run() method, then the thread terminates and the Java Virtual Machine tries to print the stack trace to the terminal window (if there is one) and the application continues to run.

You should catch all exceptions that get to the run() method and take appropriate action. This action may be giving the user a useful message telling them what happened, or more commonly you will need to pass this error information back to the application so that it can take recovery action and inform the user.

Programming with Java Threads provides a complete package for starting, stopping and handling errors in Java Threads. It shows you how to pass these errors back.

At the very least, your Thread's run() method code should include the following.

public void run() {
   try {
     ...
     // do thread work here
     ...
    } catch (Throwable t) {
       // do error recovery here
       Application.showErrorMessage(t);
       // or log the error if not critical to the user.
    }                   
}

Rule 5: Recover from Throwables

While rules 1 to 4 above are designed to catch all the errors, this rule is about recovering from them.

As mentioned above, Error Recovery is about "restoring the application to some stable state from which the user can continue". Depending on the application and the error this may be simple or complicated. Basically it involves:-
a) Recovering resources left over after the error
b) Restoring the application to some stable state. Ideally this will be the state it was in prior to the action that caused the error.

Both of these actions can often be carried out in the finally clause of a try/catch/finally block. If at all possible you should clean up at the bottom of the method that allocated the resources or changed the state, as this makes for cleaner procedural programming.

For example the actionPerformed() method of the File, SaveAs menu item should look something like this

// action for File SaveAs menu item
public void actionPerformed(ActionEvent e) {
   String saveFileName = null;   // mark as not collected yet
   FileOutputStream fos = null;  // mark resource as not allocated yet
 
   try {
     // pick up the saveFileName from user here via FileChooser dialog

     fos = new FileOutputStream(saveFileName);
     //  save to fos here
     ...
     // close output file
     fos.close();
     fos = null;  // mark as closed (resource released)

    } catch (IOException ex) {
        // handle error here  in this case rethrow it with a better message
        // See "Unified error messages" for a better way to handle this exception
       throw new IOException("Error saving file to "+saveFileName+"\n"+ex.getMessage());

     } finally {
       // always release resources i.e. close the files
       try {
           if (fos != null) {
               fos.close();
           }
        } catch (IOException ex1) { 
           // ignore this error as if fos != null we already are handling an error
        }       
       // do other clean up here
    }                   
}

In a similar way, the application state should be saved at the top of the method and then restored if an error occurs.

Rule 6: Don't just Catch and Print Throwables

Inexperienced programmers will often write code like the following in low level utility methods

private Icon makeIcon(final String gifFile) throws IOException {
    InputStream resource = HTMLEditorKit.getResourceAsStream(gifFile);
    if (resource == null) {
         System.err.println(ImageView.class.getName() + "/" +  gifFile + " not found.");
         return null; 
    }
      ......
     // rest of method here
     ....
}

Here the error is that the gifFile could not be found. There are two problems with printing to System.err.:-
1) If the application is not attached to a terminal window, the output of System.err gets lost (see LogStdStreams.java.txt for a solution to this).
2) Printing an error message to System.err and returning null limits your options for error reporting, error recovery and displaying user help.

A better approach is to write

private Icon makeIcon(final String gifFile) throws IOException {
    InputStream resource = HTMLEditorKit.getResourceAsStream(gifFile);
    if (resource == null) {
         throw new FileNotFoundException(ImageView.class.getName() + "/" +  gifFile);
    }
      ......
     // rest of method here
     ....
}

This then lets the calling method know exactly what went wrong and give it the chance to take appropriate action, for example using a default icon or displaying a detailed error message with a connected help topic.

This example is actually from the javax.swing.text.html.ImageView class. There are many places in the Java library source were errors are "handled" by just writing a message to System.err and in some cases dumping the stack trace. Some of these are just bad programming and hopefully will disappear over time. However some are due to the lack of a built in method of passing back exceptions from threads.

For your own threads you can use the thread package described in Programming with Java Threads to handle exceptions in threads but for the Java library's System.err.println() statements you will need to use LogStdStreams.java.txt to at least insure the errors are logged.

Another version of this problem is

public void doSomeThing(String input){
    ...
    try {
    // call some method here that throws a checked exception such as a BackingStoreException
    preferences.flush();
    // this method, doSomeThing, does not throw BackingStoreException so the temptation is to just catch an ignore
    } catch {BackingStoreException ex) {
       // don't know what to do so ignore
    }    
}

In this case the BackingStoreException should be wrapped in an unchecked exception such as a RuntimeException, using Java V1.4 exception chaining, ie.

public void doSomeThing(String input){
    ...
    try {
    // call some method here that throws a checked exception such as a BackingStoreException
    preferences.flush();
    } catch {BackingStoreException ex) {
       throw new RuntimeException("Error flushing preferences",ex);
    }    
}

Then you have to catch all exceptions and check their causes to see what really happened.

Bruce Eckel also mentions this temptation to “swallow exceptions” in his discussion on CheckedExceptions . He argues that checked exceptions actually encourage programmers to write code that catches and ignores exceptions.



Rule 7: Debugging code should never throw Throwables

Debugging code is supposed to assist you in checking the operation of your application and in finding and correcting errors. It is not much use if it itself throws exceptions.

The most common problem is in a class's toString() method. When an error occurs in the class, some of its member objects may be left in an invalid state (often null). Typical toString() methods try and print out the value of the class's members using object.toString().

Obviously this will throw a NullPointerException if the object is null. The preferred way of converting member objects to strings for printing is to use the StringUtils.toString() method
StringUtils.toString(object)

This method checks for null objects and then calls object.toString() and catches and returns as a String any exceptions thrown that method might throw.



Contact Forward Computing and Control by email (with anti-spam)
©Copyright 1996-2003 Forward Computing and Control Pty. Ltd. ACN 003 669 994