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.
Distinctive examples
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.
e.g. FileReadError()
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.
e.g. BadConfigurationDataError()
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.
indeed, that's what we're reached on Java and c# classes in the academic college of tel aviv jaffa. however, it takes a while until you really can tell between the cases, aside from the obvious case. especially when wrapping exception prone operation such as network resources retrieval or xml parsing.
Zion:
* On one hand it's very good to know that academy teaches such things, on the other, I hoped to write something more practical than academy 🙂
* Fully agree about the 'wrapping exception' problem. I hope to write about it as well some day.
10x for the comment
File reading problems are not exceptional but expected.
Config files having invalid configurations are expected not exceptional.
Which shows you so much of what is wrong.
maht: fair, thought provoking points - Thanks!
I would still like to lightly argue about the read_file_contents() call. By its name and the single-responsibility concept, I deduce (and maybe it's too implicit - I'll update the text) that this function isn't responsible for finding/opening the file. It relies on an earlier call (e.g. constructor) to do it. In that case, it can expect the file to be available, accessible with all permissions and successfully open already. Which renders the situation of read errors exceptional: either bad usage, real bug, or something that is really broken with the OS/hardware. All three are exceptional, imho.
Your note about the config file, parse error being expected - is very fair and interesting.
I was thinking more of an internal configuration file, and I assume that you refer to a user-modifiable configuration file. In the latter errors are obviously expected, in the former I still think they're exceptional.
So my main lesson learned from your comment is that the context matters a lot in these decisions. Thanks for this note.