Effective Use of Exceptions
July 19th, 2013 by
Throw Lots of Exceptions
It seems like a good idea, when writing new code, to use liberal amounts of thrown exceptions to signal erroneous situations. The more the better. Any time you can detect a situation where your data is completely wrong, throw an error. In Ruby just using a simple raise
with a message (or no message) can go a long way to saving your sanity.
if something_is_fishy
raise "something is fishy"
end
# or the single-line version
raise "something is fishy" if something_is_fishy
In this case your program will crash and your environment will inform you about the file and line number where the error occurred. However I have been using custom error classes (subclasses of StandardError) to more clearly describe what is wrong.
class UnexpectedlyInvalidInput < StandardError; end
class UnexpectedResponseType < StandardError; end
class ThisIsImpossible < StandardError; end
Some sources advise you to only throw exceptions which you intend to catch. I disagree. In fact most of the error throws you write should not be caught, because most of those should not be thrown at runtime anyway! Strive to use exceptions to detect situations when you, the programmer, have screwed up because something impossible has nonetheless happened. What counts as “impossible” may depend on your business requirements, documented behavior of APIs, common sense, or subjective judgement.
Obviously it will get tedious or impossible to throw errors at every stage of the code where someone may have messed up (especially in a dynamic language like Ruby). Below I will list situations where it is convenient and natural to throw.
When using a case
or a series of if, else if, else if, ...
to interpret the several possible variants of your data (such as “ok” and “error”), put a final else
, which throws an error indicating you got an unexpected variant. If you do not put the else
and you get an unexpected variant, you will spend valuable time sifting through the aftermath to determine that that was actually the cause of the error. Skip that step and you’ll be able to go fix the actual problem.
case arg['status']
when 'ok' then show_user_the_money
when 'error' then show_alert_dialog
else raise InvalidStatus
end
In an API client module you can throw an error if the raw data you get from the API is not the correct type. You expect the API to adhere to its documented behavior. If it does not, you can’t seriously expect to do anything except change your code. Throw an error.
If you are expecting a data value to be not nil
, by all means, throw an error if it is nil
before you use it. The behavior of nil
s in many programming languages, including Ruby, allows for the program to continue on willy-nilly until you get lucky enough for a crash somewhere else, maybe in another module.
def procedure_which_expects_non_nil arg
raise ArgumentError, "arg cannot be nil!" if arg.nil?
# rest of the code
end
In all of the above cases, you now have a clear directive of how to proceed if the program throws any of the errors. You need to fix your bug, alter your usage of an API, or fix a bug that produces a nil erroneously.
Don’t Worry About Catching
Don’t try to catch very much. The above errors, for example, have no valid reason to be caught. Try to stick to throwing errors, which indicate bugs to fix rather than normal conditions that will happen as a matter of course. You may need to catch at the top level sometimes to deal with low level unpredictable IO situations like a broken pipe.
It’s a Bad Idea™ to catch all errors (without re-throwing). The reason for this is that you will be painfully unaware of problems with your code that you did not expect. In Ruby, this includes syntax errors, which are not detected until late runtime and manifest themselves as exceptions!
# NO! BAD! VERY BAD!
begin
results = do_lots_of_complex_things
rescue #recover from ANY kind of exception
results = []
end
If you do rescue from all errors, make sure to re-throw simply by using raise
by itself (shown below).
# not bad
begin
results = do_lots_of_complex_things
rescue => e
send_an_error_email(e)
raise #re-throws the error e
end
# also not bad
begin
results = do_lots_of_complex_things
rescue SpecificProblem
results = []
end
The normal way to catch without re-throwing is to make sure it is only for a specific error class (shown above). Finding out which class you want to catch is tricky but worth it.