How to "try again" when exceptions happen in Ruby

Ruby provides a few interesting mechanisms that make it easy to "try again" - though not all of them are obvious or well-known. In this post we'll take a look at these mechanisms and how they work.

Not all errors are fatal. Some just indicate that you need to try again. Fortunately, Ruby provides a few interesting mechanisms that make it easy to "try again" - though not all of them are obvious or well-known. In this post we'll take a look at these mechanisms and how they work in the real world.

Introducing retry

Ok - this one is kind of obvious, but only if you know it exists. Personally, I was well into my Ruby career before I learned about the delightful "retry" keyword.

Retry is built in to Ruby's exception rescuing system. It's quite simple. If you use "retry" in your rescue block it causes the section of code that was rescued to be run again. Let's look at an example.

  retries ||= 0
  puts "try ##{ retries }"
  raise "the roof"
  retry if (retries += 1) < 3

# ... outputs the following:
# try #0
# try #1
# try #2

There are a few things to note here:

  • When retry is called, all of the code in between begin and rescue is run again. It most definitely does not "pick up where it left off" or anything like that.

  • If you don't provide some mechanism to limit retries, you will wind up with an infinite loop.

  • Code in both the begin and rescue blocks are able to access the same retries variable in the parent scope.

The Problem With retry

While retry is great it does have some limitations. The main one being that the entire begin block is re-run. But sometimes that's not ideal.

For example, imagine that you're using a gem that lets you post status updates to Twitter, Facebook, and lots of other sites by using a single method call. It might look something like this.

SocialMedia.post_to_all("Zomg! I just ate the biggest hamburger")

# ...posts to Twitter API
# ...posts to Facebook API
# ...etc

If one of the APIs fails to respond, the gem raises a SocialMedia::TimeoutError and aborts. If we were to catch this exception and retry, we'd wind up with duplicate posts because the retry would start over from the beginning.

  SocialMedia.post_to_all("Zomg! I just ate the biggest hamburger")
rescue SocialMedia::TimeoutError

# ...posts to Twitter API
# facebook error
# ...posts to Twitter API
# facebook error
# ...posts to Twitter API
# and so on

Wouldn't it be nice if we were able to tell the gem "Just skip facebook, and keep on going down the list of APIs to post to."

Fortunately for us, Ruby allows us to do exactly that.

NOTE: Of course the real solution to this problem is to re-architect the social media library. But this is far from the only use-case for the techniques I'm going to show you.

Continuations to the Rescue

Continuations tend to scare people. But that's just because they're not used very frequently and they look a little odd. But once you understand the basics they're really quite simple.

A continuation is like a  "save point" in your code, just like in a video game. You can go off and do other things, then jump back to the save point and everything will be as you left it.

...ok, so it's not a perfect analogy, but it kind of works. Let's look at some code:

require "continuation"
counter = 0
continuation = callcc { |c| c } # define our savepoint
puts(counter += 1) if counter < 5 # jump back to our savepoint

You may have noticed a few weird things. Let's go through them:

  • We use the callcc method to create a Continuation object. There's no clean OO syntax for this.

  • The first time the continuation variable is assigned, it is set to the return value of callcc  's block. That's why the block has to be there.

  • Each time we jump back to the savepoint, the continuation variable is assigned whatever argument we pass the call method. That's why we use the weird looking   syntax.

Adding Continuations to Exceptions

We're going to use continuations to add an skip method to to all exceptions. The example below shows how it should work. Whenever I rescue an exception I should be able to call skip, which will cause the code that raised the exception to act like it never happened.

  raise "the roof"
  puts "The exception was ignored"
rescue => e

# ...outputs "The exception was ignored"

To do this I'm going to have to commit a few sins. Exception is just a class. That means I can monekypatch it to add a skip method.

class Exception
  attr_accessor :continuation
  def skip

Now we need to set the continuation attribute for every exception. It turns out that raise is just a method, which we can override.

BTW, the code below is taken almost verbatim from Advi's excellent slide deck Things You Didn't know about Exceptions.  I just couldn't think of a better way to implement it than this:

require 'continuation'
module StoreContinuationOnRaise
  def raise(*args)
    callcc do |continuation|
      rescue Exception => e
        e.continuation = continuation

class Object
  include StoreContinuationOnRaise

Now I can call the skip method for any exception and it will be like the exception never happened.

Honeybadger has your back when it counts.

We're the only error tracker that combines exception monitoring, uptime monitoring, and cron monitoring into a single, simple to use platform. Our mission: to tame production and make you a better, more productive developer.

Learn more
author photo

Starr Horne

Starr Horne is a Rubyist and Chief JavaScripter at When she's not neck-deep in other people's bugs, she enjoys making furniture with traditional hand-tools, reading history and brewing beer in her garage in Seattle.

More articles by Starr Horne
“We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release.” 
Michael Smith
Try Error Monitoring Free for 15 Days
Are you using Bugsnag, Rollbar, or Airbrake for your monitoring? Honeybadger includes exception, uptime, and check-in monitoring — all for probably less than you're paying now. Discover why so many companies are switching to Honeybadger here.
Try Error Monitoring Free for 15 Days