About exceptions, here's a few rules-of-thumb off the top of my (holiday-relaxed) head:
There are three types of Exceptions that we should throw:
1. "real" exceptions, such as IOException etc., which do *not* extend RuntimeException, should be thrown wherever we can expect something to go wrong. For example, a file we need is not there, a server cannot be contacted, a file does not have the expected content. These should be marked in the method signature, and documented in the Javadoc. Example:
public void load(String filename) throws IOException
{
...
File f = new File(filename);
if (!f.exists()) throw new FileNotFoundException("File '" + filename + "' does not exist");
...
}
(remember that FileNotFoundException is an IOException)
In this category, I can think of:
IOException (general-purpose file stuff)
FileNotFoundException
SynthesisException (a MARY subclass of Exception, indicating that at run-time, synthesis failed for some reason)
OurOwnException (a self-made subclass of Exception, with a specific meaning)
Exception (if we are too lazy to define our own exception)
Also, we should not hesitate to "let through" other exceptions from third-party code we use, i.e. it is often a good idea *not* to catch such exceptions. Examples include:
DOMException thrown by XML DOM handling code;
SocketException thrown by the Socket handling code in client-server communication
... and many more.
If we prefer to catch such exceptions, we must make sure to include the original exception as a *cause* when we throw a new exception. Otherwise, it becomes impossible to track down the source of the problem. Some Exception classes offer a constructor which takes the cause as a second argument:
try {
...
} catch (IOException ioe) {
throw new Exception("Problem dumping carts", ioe);
}
Other Exception classes don't offer such a constructor (at least in Java 1.4). In such cases, we can still add the cause, e.g.:
try {
...
} catch (IOException ioe) {
IOException newIOE = new IOException("Problem dumping carts");
newIOE.initCause(ioe);
throw newIOE;
}
2. "safety check" exceptions, for the unexpected but possible case that a *public* method gets unacceptable arguments or similar. Here we throw subclasses of RuntimeException, which do not *need* to be declared; whether or not we still want to document them in the Javadoc is a case-by-case decision. The main examples are:
IllegalArgumentException (if arguments are unexpected)
NullPointerException (we should normally throw this only if an *argument* is null which should not be null)
3. And then there are *assertions*. This is for testing the unexpected case that some code *within* a method, or the arguments of a *private* or *protected* method, is not as is required. Assertion testing can be turned on or off on the command line ("java -ea" = "enable assertions"), so in trusted run-time code, assertions can be deactivated so they do not waste any time.
We should make heavy use of assertions; the rule-of-thumb is that one should write an assertion wherever one would write a System.out.println() otherwise. This may be too strict a rule, but it gives the general idea. Example:
String names[];
....
assert names != null : "Names array is not initialised";
for (int i=0; i<names.length; i++) {
assert names[i] != null : "Name " + i + " is null";
assert names[i].length > 0 : "Name " + i " is empty";
....
}
If an assertion fails, java will throw an "AssertionError" which we should *never* try to catch explicitly. If an assertion fails, something is *wrong* in the program, so processing should fail. An exception to this rule is the MARY server request handler: even if one request fails, the server should continue to run.
For assertions, I have discovered the possibility to add an error message after the colon only recently; so most of the assert statements in the MARY code unfortunately do not have an error message.
4. One thing we should normally *never* throw are Errors. These really are reserved to java-internal malfunctioning, and should normally lead to program termination.
