Ruby 'Exceptional' Knowledge

Foreward

Most of us have encountered the methods raise, rescue, catch, throw, and break in Ruby. We can generally understand what’s going on when we read code that uses it, but what exactly is the difference between them all? Here’s a quick guide plus some fun and useful facts.

If you remember just one thing…

  • raise and rescue are used for handling errors only
  • throw and catch are used in order to terminate execution early when no other work is needed. source

Raise and Rescue

raise and rescue are used exclusively for handling errors. By default, raising an error will exit the program.

exception.rb
1
2
3
4
5
6
7
8
9
def test_rescue
  puts "This is before raise"
  raise "Raised an error"
  puts "This is after raise. It won't ever run."
end
test_rescue
# => This is before raise
# => test.rb:3:in `test_rescue': Raised an error (RuntimeError)
  from test.rb:9:in `<main>'

This happens unless there is a rescue statement which will run in case of an exception.

exception.rb
1
2
3
4
5
6
7
8
9
10
def test_rescue
  puts "This is before raise"
  raise "Raised an error"
  puts "This is after raise. It won't ever run."
  rescue
  puts "I'm rescued!"
end
test_rescue
# => This is before raise
# => I'm rescued!

The above Ruby code can be rewritten like so:

exception.rb
1
2
3
4
5
6
7
8
9
10
def test_rescue
  puts "This is before raise"
  raise "Raised an error"
  puts "This is after raise. It won't ever run."
  rescue Exception => e
  puts e
end
test_rescue
# => This is before raise
# => Raised an error

Ruby has many different types of exceptions (see documentation). Raise takes up to three parameters: * the exception type * an error message * an array of callback information.

All three are optional and Ruby knows that if you only pass in a string that it’s the message. Usually you don’t set the last parameter since Kernel#caller automatically creates that array.

Here are a couple of valid raise statements.

Message parameter given
1
raise "This is an error"
Error and Message parameters
1
raise StandardError "Most error subclasses extend StandardError"

Exceptional Ruby

Exception is the root of Ruby’s exception hierarchy. It’s the class from which all Exceptions descend. It is king. This has a very interesting consequence.

rescue Exception rescues from EVERYTHING, including syntax errors, load errors, and any of the following listed below.

  • Rescuing Interrupt prevents you from being able to CTRL+C out of the program.

  • Rescuing SignalException prevents the program from responding correctly to signals. It will be unkillable, except with kill -9. source

1
2
3
4
5
6
7
loop do
  begin
    eval dinosaurs ru1ez the pl@netz!!! ROArR{bark}[:ARF].ENV
  rescue Exception
    puts "What meteor?"
  end
end

Break, Catch, and Throw

break, catch and throw are used in order to terminate execution early when no other work is needed. break leaves the current loop while the catch and throw combination can be used to break out of any number of loops at one time.

break example
1
2
3
4
5
6
array = ["brainfuck", "ruby" "befunge", "python", "perl"]
array.each do |language|
  puts "My favorite computer langauge is #{language}"
  break
end
#  => "My favorite computer langauge is brainfuck"
hypothetical throw and catch example
1
2
3
4
5
6
7
8
9
10
11
12
# => recipe_hash = {...}
def show_recipe_for(recipe_name)
  recipe = catch(:recipe) {
    recipe_hash.each do |meal_type, dish_hash|
      dish_hash.each do |dish, dish_recipe|
        if recipe_name == dish_recipe
          throw :recipe, dish_recipe
        end
      end
    end
  }
end

Notice that the two loops are enclosed in the catch block. This means that once the throw statement is executed, it will store the value of its second argument into :recipe and send it back to the catch statement. By doing so, it exits all the loops after finding the first recipe match. From there, the method finishes executing as normal.

Because my example is a bit contrived, I will post a real-life example from another blog by rubyist Avdi Grimm.

google search scraping example
1
2
3
4
5
6
7
8
9
10
11
12
13
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

Since loading pages over and over again can be an expensive process, the coder above uses a throwcatch to exit the loop when the first matching result is found.

Throw, Catch and Sinatra

An even more mind-blowing example from the same blog post reveals that Sinatra has a built-in catch for the #last-modified method. You might use this method to check a user’s cache for what version of a certain page the user has on his/her machine. Why would you do this? Simple! In order to cut out any expensive and unnecessary processing. If the page in the cache is old, then you’d update the page. Otherwise, just load from cache.

For your convenience, here’s the simplified code Grimm posted to demonstrate.

1
2
3
4
5
6
def last_modified(time)
  response['Last-Modified'] = time
  if request.env['HTTP_IF_MODIFIED_SINCE'] > time
    throw :halt, response
  end
end

When Ruby encounters the throw, it zips back up the call stack looking for a matching symbol, :halt. Where’s the catch block though? It’s clearly not in the same method as the throw. This means that it must be further up the stack. In other words, #last_modified was called within a catch block.

1
2
3
4
5
catch (:halt) do
  # code
  last_modified(time) # => the throw in this method sends :halt up to the encapsulating catch
  # code
end

Comments