Benchmarking Ruby exception handling

Which is better, handling an exception or explicitly checking to see whether or not your code is going to break? The answer is "it depends". On the one hand exception handling allows us to write more legible code (often summed up by the saying "it's easier to ask forgiveness than permission"). On the other, handling an exception is often a costly operation; it can be faster to "look before you leap".

But just how costly is exception handling? Happily it's very easy to find out. I was working away with my pair recently when this very question came up. We were calculating a ratio between two numbers, but wanted to modify the calculation so that if the denominator was 0, we'd use 0.5 instead. Our function looked something like this:

def calculate_ratio(apples, oranges)
  apples / oranges
rescue ZeroDivisionError
  apples / 0.5
end

It's succinct and, I think, very readable. We call this function a lot, and in a large percentage of those calls oranges will be 0. Just how expensive is that? There's only one way to find out. Test it!

The Benchmark module contains a few nifty functions for testing this kind of scenario. I wrote two functions, ask_forgiveness and ask_permission and ran each of them a few million times with the bm() function.

require "benchmark"

include Benchmark

LOOP_COUNT = 1000000

def ask_forgiveness(a)
  1 / a
rescue ZeroDivisionError
  1 / 0.5
end

def ask_permission(a)
  denominator = (a == 0) ? 0.5 : a
  1 / denominator
end

bm(5) do |test|
  test.report("Forgiveness:") do
    LOOP_COUNT.times do |i|
      ask_forgiveness(i % 2)
    end
  end
  test.report(" Permission:") do
    LOOP_COUNT.times do |i|
      ask_permission(i % 2)
    end
  end
end

We assumed that 50% of our function calls would result in a ZeroDivisionError, which is why we're passing i % 2 into our two functions (it alternates between 0 and 1).

And the results?

              user       system     total       real
Forgiveness:  6.780000   0.360000   7.140000 (  7.172876)
 Permission:  1.080000   0.010000   1.090000 (  1.090219)

A clear win (in this case) for checking ahead of time. It was worth benchmarking as I'd normally go with the more legible exception handling approach every time.

Exception handling is at it's best when exceptions really are the exception to the rule...

I love feedback and questions — please feel free to get in touch on Mastodon or Twitter, or leave a comment.

Published on in Ruby