Throw, Catch, Raise, Rescue- I'm so confused!
12/Jul 2011
Throw, Catch, Raise, Rescue… I’m so confused!
This guest post is by Avdi Grimm, who is the author of “Exceptional Ruby“, an in-depth guide to exceptions and failure handling in Ruby. RubyLearning readers can get a $3 discount on the book by using code RUBYLEARN. Avdi has been hacking Ruby code for 10 years, and is still loving it. He is chief aeronaut at ShipRise, a consultancy specializing in sustainable software development and in helping geographically dispersed teams work more effectively. He lives in southern Pennsylvania with his wife and four children, and in his copious spare time blogs and podcasts at Virtuous Code and WideTeams.com.
Old keywords, new meanings
One of the aspects of Ruby that often confuses newbies coming from other languages is the fact that it has both throw
and catch
and raise
and rescue
statements. In this article I’ll try and clear up that confusion.
If you’re familiar with Java, C#, PHP, or C++, you are probably used to using try
, catch
, and throw
for exception handling. You use try
to delineate the block in which you expect an exception may occur. You use catch
to specify what to do when an exception is raised. And you use throw
to raise an exception yourself.
You’ve probably noticed that Ruby has throw
and catch
… but they don’t seem to be used the way you’re used to in other languages! And there are also these “begin
“, “raise
” and “rescue
” statements that seem to do the same thing. What’s going on here?
Getting out fast
If you’ve done much programming in another language like Java, you may have noticed that exceptions are sometimes used for non-error situations. “exceptions for control flow” is a technique developers sometimes turn to when they want an “early escape” from a particular path of execution.
For instance, imagine some code that scrapes a series of web pages, looking for one that contains a particular text string.
def show_rank_for(target, query) rank = nil each_google_result_page(query, 6) do |page, page_index| each_google_result(page) do |result, result_index| if result.text.include?(target) rank = (page_index * 10) + result_index end end end puts "#{target} is ranked #{rank} for search '#{query}'" end show_rank_for("avdi.org", "nonesuch")
(For brevity, I’ve excluded the definitions of the #each_google_result_page
and #each_google_result
methods. You can view the full source at https://gist.github.com/1075364.)
Fetching pages and parsing them is time-consuming. What if the target text is found on page 2? This code will keep right on going until it hits the max number of result pages (here specified as 6).
It would be nice if we could end the search as soon as we find a matching result. We might think to use the break
keyword, which “breaks out” of a loop’s execution. But break
only breaks out of the immediately surrounding loop, and here we have a loop inside another loop.
This is a situation where we might come up with the idea of using an exception to break out of the two levels of looping. But exceptions are supposed to be for unexpected failures, and finding the results we were looking for is neither unexpected, nor a failure! What to do?
Throwing Ruby a fast ball
Ruby has given us a tool for just this situation. Unlike in other languages, Ruby’s throw
and catch
are not used for exceptions. Instead, they provide a way to terminate execution early when no further work is needed. Their behavior is very similar to that of exceptions, but they are intended for very different situations.
Let’s look at how we can use catch
and throw
to end the web search as soon as we find a result:
def show_rank_for(target, query) rank = catch(:rank) { each_google_result_page(query, 6) do |page, page_index| each_google_result(page) do |result, result_index| if result.text.include?(target) throw :rank, (page_index * 10) + result_index end end end "<not found>" } puts "#{target} is ranked #{rank} for search '#{query}'" end
This time we’ve wrapped the whole search in a catch{...}
block. We tell the catch
block what symbol to catch, in this case :rank
. When the result we are looking for is found, instead of setting a variable we throw the symbol :rank
. We also give throw
a second parameter, the search result :rank
. This second parameter is the throw’s “payload”.
The throw
“throws” execution up to the catch
block, breaking out of all the intervening blocks and method calls. Because we gave the throw
and catch
the same symbol (:rank
), the catch
block is matched to the throw
and the thrown symbol is “caught”.
The rank value that we gave as a payload to throw now becomes the return value of the catch
block. We assign the result value to a variable, and proceed normally.
What if the search string is never found, and throw
is never called? In that case, the loops will finish, and the return value of the catch
block will be the value of the last statement in the block. We provide a default value (“
catch and throw in the real world
The Rack and Sinatra projects provide a great example of how throw
and catch
can be used to terminate execution early. Sinatra’s #last_modified
method looks at the HTTP headers supplied by the client and, if they indicate the client already has the most recent version of the page, immediately ends the action and returns a “Not modified” code. Any expensive processing that would have been incurred by executing the full action is avoided.
get '/foo' do last_modified some_timestamp # ...expensive GET logic... end
Here’s a simplified version of the #last_modified
implementation. Note that it throws the :halt
symbol. Rack catches this symbol, and uses the supplied response to immediately reply to the HTTP client. This works no matter how many levels deep in method calls the throw
was invoked.
def last_modified(time) response['Last-Modified'] = time if request.env['HTTP_IF_MODIFIED_SINCE'] > time throw :halt, response end end
The way Rack uses catch/throw
illustrates an important point: the throw
call does not have to be in the same method as the catch
block.
Conclusion
Ruby is a language that tries to anticipate your needs as a programmer. One common need is a way to terminate execution early when we find there is no further work to be done. Unlike in some languages, where we would have to either abuse the exception mechanism or use multiple loop breaks and method returns to achieve the same effect, Ruby provides us with the catch
and throw
mechanism to quickly and cleanly make an early escape. This leaves begin/raise/rescue
free to be used for errors, and nothing else.
I hope you found this article valuable. Feel free to ask questions and give feedback in the comments section of this post. Thanks!