While we are talking about going overboard while following a standard, let's talk about exceptions. Exceptions were introduced late into the C++ standard and so for many years were virtually ignored. Existing code had other mechanisms, libraries did not use them and few developers understood them. Java was developed after that. The development team decided to implement and use (or dare I say overuse) exceptions.
What is an Exception?
What do you do if some code deep in the bowels of a system comes across a problem it can't handle? What, for example, should a routine to convert a string to a number do if given a non-numeric input string? I saw many exceptions in the days prior to exceptions. Some libraries set a global error number - a definite problem if you have multiple threads. I even worked on a C project not that long ago (1999) that passed an error structure around to each and every call in the system.
Then came exceptions. Just create an appropriate Throwable instance and throw it. None of the standard contiguous code that follows will be executed until some other code further up the calling graph catches exceptions based on the type you threw. That is not strictly true. Each method up the calling graph can have a finally block that will execute no matter what exceptions have been thrown. This is especially good for closing resources.
The theory is that code that does not know what to do with an error condition throws a named exception. Somewhere further up the call graph the code will know what to do or correct and will catch and deal with the problem.
public int convert( String string)
{
if (! aNumber( string))
throw new NumberFormatException( s+" cannot be made into a number");
}
...
public void process()
{
...
int result = 0;
try
{ result = convert( entry); }
catch (NumberFormatException numberFormatException)
{
/* if we can't get a conversion, make it large enough to work. */
result = 1000;
}
}
Notice that in the example,
convert() does not know what to do if the conversion cannot be done. The calling method, however, knows what sort of default to apply - and does so. In the real world it should at least write the problem to a log for later evaluation.
Problems with Exceptions
- Overuse: Current thinking is along the lines of 'if in doubt, throw an exception'. An exception is for an unexpected situation that cannot be dealt with where the problem occurred. Make sure you can't handle the problem immediately. If so, avoid an exception. Then consider how the calling code will handle it. In application code it is often the invoker who will deal with the problem. In this case there are often simpler methods that throwing exceptions to process the situation. If so, once again, no exception.
- Overuse: Yes, I know it is technically the same problem - but it's so prevalent that I have decided to repeat it. So, please read the item above again.
- Dangerous Program Flow: An exception causes program flow to jump out of line with no visual indication in the code. The lack of visual queues makes it difficult to be aware of when important operations will not happen.
file = new AppFile();
file.write( info);
file.close();
In the example above, if the write throws an exception then the file is not closed. If the code is called again it will fail because the file is open and locked.
- Insufficient Clarity: Exception pundits are quick to point out that the code immediately above should be written as:
file = new AppFile();
try
{ file.write( info); }
finally
{ file.close(); }
That's immediately more difficult to read. The natural program flow has been interrupted by the implementation
need for an exception. I prefer the earlier version.
Types of Exceptions
To be accurate, this section describes the types of errors exceptions are designed to address.
- Fatal errors. Have you noticed that when Windows 'blue screens' you can't even reboot from the keyboard. The machine needs a hard reset. This is because the system did something that is totally unrecoverable. The code that caught the error cannot even trust the keyboard, so it displays as much as it can then freezes. In client server systems the situation can be even worse. If the server cannot trust the connection to the client, the client can't even be told how or why before the freeze. Needless to say that fatal errors should be rare, since they indicate fragility in critical single-source-of-failure code. An out-of-memory exception is the most common among applications servers. While a disk-full error can be handled, it is such a rare situation that cannot be corrected by the application that it is usually treated as fatal also.
- System errors. These are the errors that are out of our control. They include disk I/O failures, unexpected closed connections and the like. Many are so unusual that it is unlikely that the code attempts to deal with it. What does your code do if a disk write returns an IOException? The conservative approach would be to treat a disk write IOException as fatal since further writing is likely to corrupt data. Most systems just write such errors to the log and report a generic system error to the user. The question is do you attempt to write again or save the data elsewhere. Again the problem is rare enough that accounting for it in the code is often overkill. Unexpected connection closures (database, socket, etc) are also system errors. In most cases they are dealt with by logging the surprise and grabbing a new connection. The worst the user will experience is a slight delay. Of course if the problem isn't monitored there is a risk of an epidemic of closures and a slow and unresponsive application.
- Inability errors. These are errors when a piece of code us unable to process with the data provided. NumberFormatException is one of these. They are almost always checked exceptions thrown with the expectation that somewhere up the calling tree code will recognise the problem and know what to do. Never rethrow an exception or pass it up the call graph without considering if the code at this point knows what to do. GUI support will recognise NumberFormatException and know to return an error message to the user. If thrown during a file read the decision may be harder. The best we can often do is log the problem and provide a reasonable default value. Too many developers treat this as a system error.
- Programming errors. These should always be unchecked. They should not have happened and the code is not built to handle it. The best solution is to undo all actions and tell give the user enough information that they can give support a full picture. I usually return an error code that is unique in the log so that support can find logging around the time of the error. NullPointerException is the way-too-common example of this class of error.
So How Do I Break the Rules?
- I always use unchecked exceptions for programming errors. Why complicate the code for situations that cannot be handled anyway.
- I use unchecked exceptions for problems I need to communicate directly to the user. Why have to explicitly use "throws" up a tree when the target is always the top?
- I only throw/catch exceptions when it is clearly the best or only way. In most cases an error condition will be more useful. If you look at my XML code in the Adept library you will see that it propulates and returns a Messages object. If there are no problems, the list will be empty. This is more useful than an exception in this situation because the code can accumulate problems rather than give up on only one.
- An expected condition is not an exception. I prefer:
if (! open( file))
tryAnother();
to
try
{ open( file); }
catch (FileNotFoundException fileNotFoundException)
{ tryAnother(); }
It's a lot more readable. My XML code works both ways to handle both sorts of clients. After processing you can ask it to throw an exception if errors were found.
- I do not consider exceptions part of the tiering system. I do not catch exceptions and rethrow them just to keep exception types within one tier.
--
Posted by Paul Marrington to Adept Software Development at 8/24/2005 04:45:00 PM