More On Exceptions
By Adrian Sutton
Benjamin still doesn’t like exceptions but I sense I’m making some headway.
Again, I think it comes to the number of paths through a piece of code. and much later:
Exceptions put a possible branch on every line of code, and that is why I consider them evil. It seems to be this belief that exceptions put a possible branch on every line of code that is making Benjamin dislike exceptions. Again though, this is just a case of exceptions making possible errors obvious. For instance, how many possible codepaths are there in the C code:
a[1] = x; The answer? Totally unlimited. Codepath 1 is where the second element of the array
a
is assigned the value in x. The second possibility is that a is uninitialized or has less than two elements in which case a part of memory will be overwritten. If that part of memory is outside the range of memory the program is allowed to write to the code will branch off into the OS’s code for dealing with illegal access. If the part of memory is allowed to be written by the program, totally indeterminate behavior will result (though most likely a segmentation fault) depending on exactly what part of memory got incorrectly overwritten. If the C compiler added bounds checking for arrays and checks for null or uninitialized variables, there would still be two branches, one where the operation completes and one where the program is forcefully terminated. That same piece of code in Java has exactly three branches. If a is null a NullPointerException is thrown and the code will branch to the first catch for NullPointerException (or one of its super-classes). If a only has 1 or 0 elements an ArrayOutOfBoundsException will be thrown and the code will branch to the first catch for ArrayOutOfBoundsException (or one of its super-classes). The final branch is where it works correctly. The moral of the story: just because exceptions aren’t thrown doesn’t mean that error conditions won’t cause branch. It’s also worth noting that without exceptions it becomes incredibly difficult (usually impossible) to recover from an invalid array access because a value is already being returned so return codes can’t be used to signify the error. The obvious response to this of course is that you shouldn’t be accessing beyond the end of an array in the first place. That’s all well and good except for two problems:
- Programmers are human and make mistakes. Eventually a bad piece of code will wind up in the product that triggers an invalid array reference. Maybe not today, maybe not tomorrow but eventually it will happen, it always does.
- You may not know ahead of time what code will be run. The second point is the most important here. Exceptions, when correctly implemented, provide a way of recovering from pretty much any type of error. Exceptions are the reason that a
Tomcat server can keep running even if one of the webapps within it terminates with an error condition. The server is capable of dealing with any given exception originating from a webapp by catching it and restarting or disabling the webapp. You simply can’t do that with return codes because return codes depend on every little piece of code behaving correctly and actually checking the return code. Besides all this, the best way to measure code complexity is to measure the number of code blocks. A new code block is created for each if statement, else, for, while, do, catch and finally. Finally blocks are special here, they count 1 for the case where the code exits normally and 1 for each catch block for that try statement. The reason for this is that’s the number of ways the finally block can wind up being executed. You may think that this shows clearly that exceptions add to complexity, but they don’t. Lets look at an example:
try {
doStuff();
} catch (Exception e) {
// Recover or die or something.
} Here we have two code blocks. The contents of the try and the catch statement. Now lets look at the return code version:
int result = doStuff();
if (result != 0) {
// Recover or die or something.
} Here we have two code blocks. The code before the if and the if block. So try/catch blocks don't add complexity, they simply make the complexity more obvious. What about finally blocks though? They still don't add complexity:
try {
doStuff();
} catch (Exception e) {
// Recover or die or something.
} finally {
cleanUp();
} 4 code blocks. The try, the catch and 2 for the finally (one when it works, one when it doesn't).
int result = doStuff();
if (result != 0) {
// Recover or die or something.
}
cleanUp(); 3 code blocks. Simpler right? Wrong, we've simply hidden the fact that cleanUp() may be called in two completely different scenarios. To make this clear you'd have to write the code as:
int result = doStuff();
if (result != 0) {
// Recover or die or something.
cleanUp();
} else {
cleanUp();
} which is just ugly (though admittedly has only 3 code blocks still). Of course, it would be possible to achieve the same thing with exceptions in 3 code blocks:
try {
doStuff();
} catch (Exception e) {
// Do stuff
}
cleanUp(); The finally version however is the best because it ensures that cleanUp will be called even if the recovery code returns from the method and because it makes it extremely obvious that the cleanUp method must be run no matter what. To answer some specific questions:
Unfortunately, in both C++ and Java when you arrive in the finally {} block you don’t know exactly how you got there. Did you commit that transaction, or didn’t you? Can we consider the function to have executed sucessfully? Did the exception come from a method you expected to raise the exception, or was it something that (on the surface) looked innocuous? Did you commit the transaction? It doesn’t matter. Finally blocks are only used for things that must occur whether or not the try block was completed successfully. In other words, if you need to know whether or not you completed successfully, you shouldn’t be using a finally block. Same answer for can we consider the function to have executed sucessfully? Regarding what caused the exception, in a finally block you shouldn’t care (see above). In the catch clauses, it’s easy to tell. You can catch specific types of exceptions and you can put as much or as little as you like inside the try block. That’s exactly why it’s bad form to use catch (Exception e) unless you really want to catch any error because the code is a plugin or webapp etc (in which case you should be catching Throwable and not Exception).
These are all issues that you have to consider when using function return codes to convey success (or otherwise) of operations, but with an exception your thread of control jumps. The code that triggered the exception does not (may not) know where it has jumped to, and the code that catches the exception does not (may not) know where it came from. Neither piece of code may know why the exception was raised. The code that triggered the exception could have caught it itself and managed it if desired, it chose to throw that exception back out (explicitly in the case of checked exceptions). Actually, at this time it’s probably worth explaining the details of how to use checked and unchecked exceptions. Checked exceptions should be used whenever the error is unexpected – ie: when it is not programmer error. IOException is checked because the ability to read and write from a file or socket can’t be determined ahead of time. On the other hand, unchecked exceptions are used when the programmer should have known better. NullPointerException is unchecked because a programmer shouldn’t write code that attempts to dereference null pointers. The reason I point this out is that unchecked exceptions are all the things that can be prevented by checking preconditions and checked exceptions are all the things that can’t be prevented (and hence absolutely must be handled in some form). So if you’re confident enough that you’ve checked all your exceptions and that nothing will go wrong (you have to be if you aren’t using exceptions because that’s all that stands between you and total disaster) then you can also guarantee that you won’t get any unchecked exceptions. Checked exceptions on the other hand are all those things that you would have had to put in checks for return codes for anyway and are definitely not unexpected, in fact with Java they’re declared as part of the method signature.
The main thing I do is to minimise error conditions by making things preconditions instead. This removes the need for both exception handling and return code handling Absolutely false. If you minimize error conditions you have only minimized the usage of exception or return code handling. You have not removed the need for one of them at all. This is extremely important as it’s the crucial advantage that exceptions provide – they force you to deal with them. You can’t ignore an exception, if you don’t catch it the thread will be terminated. Return codes however can be ignored very easily and provide absolutely no indication that it has been ignored.
Once data has passed through that set objects no longer handle errors. Any error past that point is a bug in my program, not the other guy’s. A bug in my program must terminate my program. This sounds like a recipe for disaster to me. This assumes that every line of code you write is perfect, every library you use behaves as expected and is completely bug free and that nothing will ever go wrong. I want to live in that world… In the world I live in, bugs happen at the most unexpected of times in code that has been reviewed a million times. Libraries perform as expected during development and testing and then break under production conditions (or new versions). In other world, in my world unexpected things happen and we need to deal with them. Ignoring errors or assuming errors can’t happen is an extremely bad software development practice. In general though the concepts are good – you should check user input as soon as possible when it comes into the system, you should check for library and OS errors as soon as possible, but you should also assume that random errors will occur in your program and make sure it can handle it somehow (even if that means saving user data and then crashing out). It sounds like for the application you’re currently working on the best option in the face of any error is to just terminate the process. That’s not a particularly common case. Most programs want to catch even fatal exceptions and log as much information as possible, store user data for recovery when the process is restarted or releasing resources like file system locks, open sockets etc. Exceptions allow this handling to be done simply and safely because unhandled exceptions will automatically propagate up. Unhandled return codes on the other hand will result in indeterminate behavior.
Exceptions add more code paths, ones that don’t go through the normal decision or looping structures of a language. If you’ve never really used if statements before, an if statement doesn’t go through the normal decision or looping structures of a language. The fact is, exceptions are a standard part of the Java language, they’re very heavily embedded and they are as straight forward as if statements to check return codes. C++ exceptions on the other hand were tacked on after the initial language design (like everything in C++) and as such probably don’t make as much sense. All I can really suggest is that you actually work with exceptions for an extended period of time to see exactly how they can work. If you still feel that they are unintuitive or not a standard part of the language, you haven’t spent enough time with a good implementation of exceptions. There are definitely a lot of bad things that people have done with exceptions and they do have their down sides (particularly when a lot of different types of checked exceptions can be thrown), but overall they are definitely a net benefit in my experience.
Without that visual cue that a particular code path exists, and without a way to minimise the number of paths through a particular piece of code, I’m extremely uncomfortable. The visual cue is the try { } catch() block, it’s very distinctive and very obvious. The point at which the exception is simply an error conditioned being encountered, it would have happened anyway, it’s not a new branch point.
Exceptions put a possible branch on every line of code, and that is why I consider them evil. No, exceptions only cause a branch when an error occurs. An error can occur regardless of whether or not an exception is thrown. With exceptions however, you at least know about it. Remember, exceptions are just like return codes but obvious and you can’t just ignore them. Exceptions don’t magically appear out of nowhere, they are simply a special return code channel specifically for errors.