Ideally error handling should not be done at all. Instead programs should have different pathways according to state. For example, you might normally have a success path and a failure path, but in some circumstances you might have a tri-state path or a single state path when errors are not expected. This way of doing programming is possible in first-class languages such as Lisp by using continuation passing, however, practical considerations currently prevent Lisp from being used widely for commercial development.
In the procedural programming style found in C and Java error handling could be improved by modeling methods by return value type and adding an error buffer to the stack. When a failure occurs the programmer has the option of adding text to the error buffer before returning false/null. In this system you might have the following method models:
* binary (returns true on success, false on failure, has buffer)
* tertiary (returns true on success, false on failure, partial on qualified success, has buffer)
* value (returns value, has buffer)
* immediate (returns value, no buffer)
* void (returns nothing, no buffer)
In the first case no keyword would be used--it would be the default. In the other cases a keyword would be required. The buffer could be accessed off the current object instance or off the thread in the case of a static method. Immediate and Void methods could be used when errors are not expected. In this case if an error occurred a global error handler would be called. If I were designing a language I would make binary the default method mode, but would also allow programmers to declare an entire package or class to default to a particular method mode. For example, the programmer could write "mode immediate" at the beginning of a class and all the methods would default to immediate in the class.
A typical binary method (which should be the usual case in a well-written program) would look like this:
static binary zLoadString( java.io.InputStream inputstreamResource, StringBuffer sbResource ){
if( inputstreamResource == null ){
return error("resource not found");
}
do {
on error: return error("Failed to read resource");
BufferedReader brFileToBeLoaded = null;
int iFileCharacter;
brFileToBeLoaded = new BufferedReader(new InputStreamReader(inputstreamResource));
while(true) {
iFileCharacter = brFileToBeLoaded.read();
if(iFileCharacter==-1) break;
sbResource.append((char)iFileCharacter);
}
} finally {
on error: return error("Failed to close resource");
if(brFileToBeLoaded!=null) brFileToBeLoaded.close();
}
return success;
}
Compare this to the same method written in Java as it exists today:
static boolean zLoadString( java.io.InputStream inputstreamResource, StringBuffer sbResource, StringBuffer sbError){
if( inputstreamResource == null ){
sbError.append("resource not found");
return false;
}
BufferedReader brFileToBeLoaded = null;
try {
int iFileCharacter;
brFileToBeLoaded = new BufferedReader(new InputStreamReader(inputstreamResource));
while(true) {
iFileCharacter = brFileToBeLoaded.read();
if(iFileCharacter==-1) break;
sbResource.append((char)iFileCharacter);
}
} catch(Exception ex) {
sbError.append("Failed to read resource: " + ex);
return false;
} finally {
try {
if(brFileToBeLoaded!=null) brFileToBeLoaded.close();
} catch(Exception ex) {
sbError.append("Failed to close resource: " + ex);
return false;
}
}
return true;
}
From the above you can see having moded methods would lead to terser code as well as built-in support for the error buffer. The current methodology is inferior on a number of different counts. For example, re-throwing errors is never done because it is so unwieldy so having nested catch statements does not make sense. Also, combining catch with finally is ad hoc because error handling and finalization are two separate things. The methodology I propose above correctly separates finalization from error handling and makes the error statements linear instead of nested which is natural. By having method modes it is possible to return errors directly (insted of having to do it in two steps). Java proponents might claim you can do it in Java in one step by throwing an error but this is not strictly true because when you throw an error the upstream caller does not know which line of code failed. It is important that the upstream caller be able to test return values for failure in order to return an accurate error message.
As far as I am concerned the end goal is an accurate error message which describes the error and gives the context in which it occurred. This is where try-catch and global handlers fall down: they lose the context of the error. With try-catch many lines of code are enclosed and the actual failing line is not identified unless a stack trace is printed. Printing stack traces is a bad solution for two reasons: (1) it is meaningless and scary to users and (2) much relevant information is lost. If the occurred in a loop, which item was it? the first, the last, the 439th?, or if it was a file which file? etc stack traces do not tell these pieces of information which may be upstream from the point of error. This is why it is important to accumulate messages in an error buffer. Currently no major computer language that I am aware of supports such an error message buffer.
You can see my solution from the second of the two code examples above: I pass a string buffer into all my error-handled routines to accumulate the error. A much better solution would be to support this buffer within the syntax of the language instead of using the ham-handed exception methodology.
Maybe in a future world programmers will have this ability.