Exceptions should represent conditions that it's likely the immediate calling code will not be prepared to handle, even if the calling method might. Consider, for example, code which is reading some data from a file, may legitimately assume that any valid file will end with a valid record, and is not required to extract any information from a partial record.
If the read-data routine didn't use exceptions but simply reported whether or not the read succeeded, the calling code would have to look like:
temp = dataSource.readInteger();
if (temp == null) return null;
field1 = (int)temp;
temp = dataSource.readInteger();
if (temp == null) return null;
field2 = (int)temp;
temp = dataSource.readString();
if (temp == null) return null;
field3 = temp;
etc. spending three lines of code for each useful piece of work. By contrast, if readInteger
will throws an exception upon encountering the end of a file, and if the caller can simply pass on the exception, then the code becomes:
field1 = dataSource.readInteger();
field2 = dataSource.readInteger();
field3 = dataSource.readString();
Much simpler and cleaner looking, with far greater emphasis on the case where things work normally. Note that in cases where the immediate caller would be expecting to handle a condition, a method which returns an error code will often be more helpful than one which throws an exception. For example, to total all the integers in a file:
do
{
temp = dataSource.tryReadInteger();
if (temp == null) break;
total += (int)temp;
} while(true);
versus
try
{
do
{
total += (int)dataSource.readInteger();
}
while(true);
}
catch endOfDataSourceException ex
{ // Don't do anything, since this is an expected condition (eventually)
}
The code which asking for the integers is expecting that one of those calls is going to fail. Having the code use an endless loop which will run until that happens is far less elegant than using a method that indicates failures via its return value.
Because classes often won't know what conditions their clients will or will not expect, it's often helpful to offer two versions of methods that could fail in ways that some callers will expect and other callers won't. Doing so will allow such methods to be used cleanly with both types of callers. Note also that even "try" methods should throw exceptions if situations arise the caller probably isn't expecting. For example, tryReadInteger
should not throw an exception if it encounters a clean end-of-file condition (if the caller weren't expecting that, the caller would have used readInteger
). On the other hand, it probably should throw an exception if the data could not be read because e.g. the memory stick containing it was unplugged. While such events should always be recognized as a possibility, it's unlikely that the immediate calling code would be prepared to do anything useful in response; it should certainly not be reported the same way as would be an end-of-file condition.