Ever since I've been using exceptions, uncertainty has always been there: when are we better off with return codes? when is it smart to catch the exceptions? when should they better be left untouched and propagate higher? Create a separate exception class, or just pass an argument to the constructor?
During the recent few years, I've been gathering some best practices I learned from my peers and by experience, which I'll dump here to a short series of posts. I hope it'll help to provoke some discussion on this rather important design matter. As it's a highly debatable matter, discussions are more effective than trying to declare the ultimate truth of proper usage of exceptions.
Most of my experience in this area is related to Python, but I believe the same discussion usually applies to all dynamic languages, and possibly to languages that use throw rather than raise.
Enough disclaimers, let's get started. An easy one for starters:
Part 1: Should a function raise an exception or return a failure value?
Exceptions, as their name suggests, should be raised in exceptional cases. To differentiate between exceptional and normal, I usually ask myself what's the purpose of the function. If a function is well-named (it should be!), it gets easy to deduce that. [I also find TDD very helpful in deciding what's the purpose of a function, and thus how to properly name it]
To sum up my take on this, in a single sentence:
Exceptions should be raised only when the function encounters a case that is out of the scope of its purpose.
Do raise an exception - something out-of-scope has occurred:
- read_file_contents(file_handle), may raise an exception if file handle is closed or if read has unexpectedly failed.
Update: My assumption here is that the file open operation occurred earlier and succeeded; this makes read errors exceptional and not expected.
- parse_configuration(data) may raise an exception if given data is bad.
Update: I refer here to an internal not-user-modifiable configuration file, which renders a parsing failure exceptional and not expected.
Don't raise (return a value) - something normal, in-scope, has occurred:
- file.is_open() should NOT throw an exception if file is not open: its name suggests its a boolean function, therefore it should return False. A closed file is a normal case for this function.
apples.count() - if apple count is 0, I would expect the function to return 0, rather than raising ZeroApplesError() exception. Zero apples is a normal case for a the scope of the count() function.
But it's not always that obvious! In the larger context (e.g. an apple juice factory control program), zero apples may seem like an exceptional case - but we should look at the scope of the function and not beyond. In this hypothetical juice factory case, it WILL make sense to raise an exception at a higher-level function such as squeeze_apples(), if apples.count() == 0.
Of course in reality we encounter cases which are harder to differentiate, but I find it useful to compare the real cases to these fictional edge cases. Furthermore, even these edge cases DO happen sometimes...
Do you agree? disagree? Do you find cases where even the most unexpected error should return -1 rather than raise an exception? Add your comments.