How to Retry in Ruby

How to Retry in Ruby

As developers, you must have observed how several code operations and requests by their very nature are likely to fail a few times arbitrarily. This could be a result of network overloads, bottlenecks, latency, and other common application issues. 

So the idea broadly here is to acknowledge such scenarios and have mechanisms in place that can allow us to repeat code operations, for example – when dealing with exception handling blocks, loops, and anywhere else. Repeat stuff by going back in code — that’s the theme around which the introduced Ruby concepts will revolve. 

Ruby shines in this regard by providing several control flow keywords to make it easier for developers to have more freedom and flexibility over their program. So in this post, we will look at three super helpful Ruby concepts that allow developers to utilize these mechanisms to write cleaner, more effective code.

We will be discussing redo, retry, and continuations in Ruby.  We’ll talk about what they are, how they are used, cover some common use cases, and clear some of the confusions surrounding these keywords.  

Here’s an outline of what we’ll be covering so you can easily navigate or skip ahead in the guide – 

Ruby Retry 

As the name suggests, retry allows you to retry running a block of code.

begin
  raise # an exception
rescue
  retry # ⤴
end

Retry is used primarily in the context of exception handling in Ruby. When your program encounters an exception inside a begin block, control moves to the rescue block where the exception is handled. This is conventionally followed by the execution of the code below the rescue block. So if you think about it, the code in the begin block gets only one chance to do its thing.

Some functions deserve a second chance. There can be several use cases for wanting your begin block to make amends.  For example, HTTP requests and database calls can sometimes fail or time out at their first go due to an overloaded server taking longer to respond. In such cases, you might want to rerun the block a few more times before you give up. 

This can be easily achieved using the retry statement baked into Ruby’s exception handling system. Let’s see an example –

# retry.rb
begin
  attempts ||= 1 # keeping track of retries
  
  puts "doing arbitrary task (attempt ##{ attempts })"
  raise "an exception occurred" # manually raising error

rescue => error
  puts error.message
  
  if (attempts += 1) < 5 # go back to begin block if condition ok
    puts "<retrying..>"
    retry # ⤴
  end
  
  puts "-------------------------"
  puts "Retry attempts exceeded. Moving on."
end

In the code above, we define a begin block where we vaguely simulate a code operation that raises an error. When handling this error in the rescue block, we check for the number of attempts (tracked using the attempts variable) and choose to retry and go back or move ahead. Upon calling retry, all of the code inside the begin block is executed again. Below is what the output looks like —

$ ruby retry.rb
doing arbitrary task (attempt #1)
an exception occurred
<retrying..>
doing arbitrary task (attempt #2)
an exception occurred
<retrying..>
doing arbitrary task (attempt #3)
an exception occurred
<retrying..>
doing arbitrary task (attempt #4)
an exception occurred
-------------------------
Retry attempts exceeded. Moving on.

A few things to mention here –

This was just a simple example to demonstrate how retry can be used. As you can imagine, we can similarly extend this for more practical use cases like the network request time-outs we mentioned previously. Let’s look at an example of that. 

Retry Example

Here, we’ll take a more practical example to see retry in action. We’ll try to make an HTTP request to a (non-routable) URL and test our explicit retry attempts when the request times out.

# retry-2.rb
require "net/http"

http = nil
uri = URI("https://www.google.com:81") # a URL that we know will time our request out

begin
  attempts ||= 1
  unless http
    puts "Opening TCP connection to #{uri.to_s}"
    http = Net::HTTP.start(uri.host, uri.port, open_timeout: 10) # error           raised here
  end
  puts "Making HTTP GET request..."
  puts http.request_get(uri.path).body

rescue Net::OpenTimeout => e # catching timeout exception
  puts "Timeout: #{e} (attempt ##{ attempts })"

  if (attempts += 1) <= 5 # retry if condition ok
    puts "<retrying..>"
    retry # ⤴
  else
    puts "--------------------------"
    puts "Retry attempts exceeded. Moving on."
  end

ensure
  if http # if the request is successful
    puts "Closing the TCP connection..."
    http.finish
  end
end

Here, after attempting to open the connection in the begin block, we rescue the timeout exception and retry our request four additional times before we finally move ahead. Below is what the output looks like-

$ ruby retry-2.rb
Opening TCP connection t https://www.google.com:81
Timeout: execution expired (attempt #1)
<retrying..>
Opening TCP connection to https://www.google.com:81
Timeout: execution expired (attempt #2)
<retrying..>
Opening TCP connection to https://www.google.com:81
Timeout: execution expired (attempt #3)
<retrying..>
Opening TCP connection to https://www.google.com:81
Timeout: execution expired (attempt #4)
<retrying..>
Opening TCP connection to https://www.google.com:81
Timeout: execution expired (attempt #5)
--------------------------
Retry attempts exceeded. Moving on.

One possible limitation of retry is that it re-runs everything inside your begin block. There could be cases where you might want to retry only the erroneous statement instead of the whole block. Therefore, this is just something developers must keep in mind when writing and structuring their exception handling code (for instance, opting for more targeted begin blocks to overcome the above limitation).

Now let’s look at another keyword in Ruby that operates with similar philosophy but in a different context.

Ruby Redo 

Ruby’s redo allows developers to repeat (or redo) loop iterations. You can think of it as a retry for loops instead of for begin/rescue blocks.

while condition1 do
    if condition2
        redo # the current iteration ⤴
    end
end


Even though it’s pretty straightforward, I think it helps contrast redo with other control flow operators like break and continue.
break terminates and exits the loop altogether, whereas continue terminates the current iteration and moves to the next one. redo, on the other hand, repeats the current iteration.

Redo Example

Let’s do an example. We’ll use the redo operator to add multiple entities (fruits) from one array to another (cart). 

# redo.rb
fruits = ["banana", "apple ", "mango", "kiwi"]
cart = []
counter = 0 # for tracking number of redos
for fruit in fruits
    puts "Added #{fruit}  to cart: #{cart.append(fruit)}"
    if fruit == 'apple' # arbitrary condition check
        redo if (counter += 1) < 4 # ⤴ redo three more times
    end
end

As you can see here, we redo the adding of an apple to our cart three more times based on a counter variable used for tracking the number of redos. Below is what our code’s output looks like–

$ ruby redo.rb
Added banana to cart: ["banana"]
Added apple to cart: ["banana", "apple"]
Added apple to cart: ["banana", "apple", "apple"]
Added apple to cart: ["banana", "apple", "apple", "apple"]
Added apple to cart: ["banana", "apple", "apple", "apple", "apple"]
Added mango to cart: ["banana", "apple", "apple", "apple", "apple", "mango"]
Added kiwi to cart: ["banana", "apple", "apple", "apple", "apple", "mango", "kiwi"]


As you can imagine, what Ruby’s redo brings to the table is not something that quick workarounds can’t easily achieve. However, having an operator that can simplify things always helps in writing cleaner and more efficient code.

Since Ruby 1.9, redo only works within loops (like retry only works within exception handling). However, you can perhaps imagine a way to use redo to emulate the functionality of a retry when used in conjunction with begin/rescue blocks. 

Redo like Retry: An Example

Let’s revisit the second last example where we retried HTTP requests that timed out. Now suppose if we were to make requests to multiple URLs and reattempt these upon timeout. This will allow us to bring a loop in the scene and use our redo operator for retrying. Let’s look at an example –

# redo-2.rb
require "net/http"

http = nil
uris = [URI("https://www.google.com:81/"), URI("https://www.google.com/")] # one url that will timeout and another that won't

for uri in uris
    begin
        attempts ||= 1
        puts "Opening TCP connection to #{uri.to_s}"
        http = Net::HTTP.start(uri.host, uri.port, open_timeout: 5)
        puts "Connection object: #{http}”

    rescue Net::OpenTimeout => e # catching timeout exception
        puts "Timeout: #{e} (attempt ##{ attempts })"
        if (attempts += 1) < 5 # redo if condition ok
            puts "<redoing..>"
            redo # ⤴
        else
            puts "--------------------------"
            puts "Redo attempts exceeded. Moving on."
        end
    ensure
        if http # if the request is successful
            puts "Closing the TCP connection..."
            puts "--------------------------"
            http.finish
        end
    end
end

 Here, we try to make HTTP requests to two URLs (one that will timeout, another that won’t) in an array and utilize the redo operator for repeating the requests upon timeout. This is similar to our second last example, except that we are iterating over multiple URLs here through a loop – which allows us to replace retry with redo. Below is the output of the code–

$ ruby redo-2.rb
Opening TCP connection to https://www.google.com:81/
Timeout: execution expired (attempt #1)
<redoing..>
Opening TCP connection to https://www.google.com:81/
Timeout: execution expired (attempt #2)
<redoing..>
Opening TCP connection to https://www.google.com:81/
Timeout: execution expired (attempt #3)
<redoing..>
Opening TCP connection to https://www.google.com:81/
Timeout: execution expired (attempt #4)
--------------------------
Redo attempts exceeded. Moving on.
Opening TCP connection to https://www.google.com/
Connection object: #<Net::HTTP:0x00007fc9c98d5b68>
Closing the TCP connection...
--------------------------

Ruby Continuations

Now let’s look at a mechanism slightly different and unconventional from those that we have been discussing so far – Ruby’s continuation.

As you can see, retry and redo allow us to move a program’s control backward – either to the preceding begin block or to the start of a loop’s iteration. However, these statements enforce certain constraints in that the control can flow back only to predetermined points in your code. This means that, conventionally, there isn’t complete freedom for developers in choosing how far back they would like the code’s control to flow. This is where continuations come into the picture.

Continuations enable storing a computation state that can be returned to anytime during a program’s execution – from anywhere in your code. It represents a current point of execution in your program. You can think of it as a checkpoint in your code’s execution that you can jump back to from anywhere you like. This is likely to remind you of the goto and setjmp/longjmp statements from the C language.

Continuations might seem somewhat complicated at first but can be easily understood using a couple of simple examples.

Continuation Examples

# my_continuation.rb
require "continuation" # explicit import


ctr = 0
my_cont = callcc { |c| c } # define continuation checkpoint ⬅
puts “Counter: #{ctr += 1}” # ⬇
my_cont.call(my_cont) if ctr < 5 # call (go back to) checkpoint ⤴

Here, as you can see, we need to export the continuation library into our code explicitly. This is because the callcc function we need to use has been declared obsolete by Ruby. 

The callcc (short for ‘call with current continuation’) is a Kernel method that takes a block and is used to create a continuation object (my_cont). Now, each time we use the continuation to jump back to our previous checkpoint, we pass through the call function the continuation object itself that is then assigned to my_cont. Using this, we are able to recurse back to our checkpoint until the counter runs out of its limit (like in previous examples). Note the arrows in the code’s comments to understand the control flow of the example.

Below is the output of our code –

$ ruby my_continuation.rb
/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/universal-darwin20/continuation.bundle: warning: callcc is obsolete; use Fiber instead


Counter: 1
Counter: 2
Counter: 3
Counter: 4
Counter: 5

Let’s take a look at another example. Here, we’ll also try to pass an arbitrary parameter back to our continuation checkpoint. 

# my_continuation-2.rb
require "continuation"

ctr = 0
my_cont = nil

puts "The call/cc function returned: #{callcc { |c| my_cont = c;}}" # ⬅
# ⬇
ctr += 1
# ⬇
my_cont.call("hello world (#{ctr})") if ctr < 3 # ⤴
my_cont.call("jello world (#{ctr})") if ctr < 6 # ⤴

if ctr < 9
    my_cont.call("mellow world (#{ctr})") + 0 # ⤴
    puts "I am not executed!"
end

Here, as you can see, we pass strings back to our checkpoint and play around with different counter values. One interesting thing to note here would be how even the ‘+ 0’ in the third from the last line doesn’t raise a TypeError (“no implicit conversion of Integer into String”). This is because the control is already sent back to our continuation checkpoint before the latter executes. Below is what the output looks like –

$ ruby my_continuation-2.rb
/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/universal-darwin20/continuation.bundle: warning: callcc is obsolete; use Fiber instead

The call/cc function returned: #<Continuation:0x00007fa981093350>
The call/cc function returned: hello world (1)
The call/cc function returned: hello world (2)
The call/cc function returned: jello world (3)
The call/cc function returned: jello world (4)
The call/cc function returned: jello world (5)
The call/cc function returned: mellow world (6)
The call/cc function returned: mellow world (7)
The call/cc function returned: mellow world (8)

Here, callcc returns the continuation object the first time and the passed strings subsequently.

So, a continuation essentially allows us to continue from an earlier position in our code and can be thought of as just another helpful control statement. However, it is worthwhile to note that due to several criticisms against call/cc cited by experts, you are significantly less likely to find continuations being used in production.

Nevertheless, it’s always helpful to be aware of language features that provide developers with more control over their code, allowing them to understand better the abstractions and the purpose they serve.

Closing Thoughts

In this post, we looked at three relatively less popular (but super helpful!) mechanisms in Ruby that grant more freedom to developers in their program’s control flow and the code they can write. We discussed the retry keyword – which gives our begin block another chance when an exception is caught using rescue. After going through a couple of its examples, we looked at the redo keyword and how it can be used to repeat loop iterations. In the end, we covered Ruby continuations, how they work, and what they bring to the table.

Now that you have a good understanding of these concepts go ahead and try them out in your code. Think about where these mechanisms could be utilized in your applications, implement them, and notice how they could make things simpler.

To learn more about working with exceptions in Ruby, you can check out the Exception Handling in Ruby post on our blog. And if you are serious about getting the most out of your Ruby applications, getting actionable insights for optimizing performance,  live alerts about potential issues, bottlenecks, and lots more, check out ScoutAPM and get started with a 14-day free trial.