Consider a module that does some IO. The module might be used in different kinds of apps, both GUI apps, services and command line apps. How should this module handle a "disk is full"-error? Should it show a dialog with a warning, or write the message to the console and die? The appropriate action would be completely different depending on the type of app and the context wherein the module is invoked.
Exceptions allows the module to delegate the error handling to the right level of abstraction.
You need to do the explicit propagation after every single line that might generate an error. That seems to me to lead to a lot of boilerplate code, and it would be easy to forget or mess up one of these lines.
I kind of see Joels point though, that if you write:
openFile();
doSomething();
closeFile();
Then it is not obvious by just looking at these lines in isolation, that the file may not be released. If you already have the throws clause in the signature for some other reason, you might miss this.
I don't think boilerplate error handling is the right solution for this though. Boilerplate just gives you boilerplate-blindness.A language for programming nuclear devices like Joel hints at, might make the throws clause explicit on every method call:
openFile();
doSomething() throws IOErrors;
closeFile();
(Don't know if any language has this syntax, but at least it makes the throw obvious in a much safer way.)However I prefer something like the using pattern in C#:
using (file) {
doSomething();
}
(Same as "with" in python.)
I find this the ideal solution, since you don't have to worry about whether the method may throw exceptions at all, the file will always close correctly. And regardless of exceptions, the syntax guarantees that you don't forget to close the file. Say you write a function that takes input given by the user, inserts it into a list, and then sends the action across the network. Every item has an ID, so when the function finishes, the current ID number needs to be incremented.
add_item (item) {
var item_id = current_id;
insert_item(item, item_id);
update_current_id();
send_item(item, item_id);
}
You write the function, and it works, and no one has any problems with it.Two months later, you come back to the function and think, "That's messy. I should clean that up." So you do.
add_item (item) {
insert_item(item, current_id);
send_item(item, current_id);
update_current_id();
}
Do you see a problem with this code? No, you don't. You even run the program and try it out, and everything seems to work fine.What if the function header had said "throws NetworkException"? You might have been more careful, but I wouldn't count on it. If the function were longer, which it would be in real life, you probably wouldn't catch your mistake.
On the other hand, what if the code looked like this?
add_item (item) {
if (insert_item(item, current_id) == INSERT_ERROR) {
return INSERT_ERROR;
}
if (send_item(item, current_id) == SEND_ERROR) {
return SEND_ERROR;
}
update_current_id();
return SUCCESS;
}
Do you see the problem now? Of course. It's staring you in the face: current_id won't get updated if the network goes down. The result is that you could have two items in your list with the same ID.This isn't "nuclear power plant" paranoia. What I'm describing is a real bug I had recently. It wasn't the first time that I've done something like this, either.
I like your idea of putting the throws clause next to the function call. I'd like to see something like that for checked exceptions, and eliminate unchecked exceptions completely.
I still prefer to write exception-based code, but I understand how you could prefer to use return values.
The point is that there is no such thing. Exceptional situations should always be handled locally, and by locally I mean on the very next line. If they can't be, then the program should stop running.
Writing code with exceptions is very hard to get right. Not checking return values is just stupid.