Ensuring execution, retrying failures and reraising exceptions in Ruby
Tags: ruby, exceptions, beginners, learning
Published: 2018-05-16 13:34:06

---
title: Ensuring execution, retrying failures and reraising exceptions in Ruby
author: Jeff Kreeftmeijer
description: Besides `rescue`, Ruby has more ways to handle exceptions. We'll use the `retry` and `ensure` keywords and reraised exceptions to build a resilient web API client.
cover_image: https://blog.appsignal.com/images/blog/2018-05/retry.jpg
canonical_url: https://blog.appsignal.com/2018/05/16/ensure-retry-and-reraise-exceptions-in-ruby.html
tags: #ruby #exceptions #beginners #learning
published: true
---

Raised exceptions can be [rescued](https://blog.appsignal.com/2018/04/10/rescuing-exceptions-in-ruby.html) to execute an alternative code path when things go wrong, but there are more ways to handle exceptions. In this edition of AppSignal Academy, we'll go over the `retry` and `ensure` keywords, and we'll look at reraising rescued exceptions.

Let's pretend we're communicating with an unreliable web API. Aside from it being down every once in a while, it's so slow that requests to it can take seconds. Our library depends on this API, and we need to make it as resilient as possible.

## `ensure`

The `ensure` keyword is used for *ensuring* a block of code runs, even when an exception happens.

In our library, we'd like to ensure the TCP connection opened by `Net::HTTP.start` is closed, even if the request fails because it times out, for example. To do this, we'll first wrap our request in a `begin`/`ensure`/`end` block. The code in the `ensure` part will always run, even if an exception is raised in the preceding `begin` block.

In the `ensure` block, we'll make sure to close the TCP connection by calling `Net::HTTP#finish` unless the `http` variable is `nil`, which can happen opening the TCP connection fails (which will also raise an exception).

``` ruby
require "net/http"

begin
  puts "Opening TCP connection..."
  http = Net::HTTP.start(uri.host, uri.port)
  puts "Sending HTTP request..."
  puts http.request_get(uri.path).body
ensure
  if http
    puts "Closing the TCP connection..."
    http.finish
  end
end
```

*Note*: We close the TCP connection manually to allow us to use the connection when retrying later. However, since `Net::HTTP.start` takes a block which handles ensuring the connection is closed, the sample above can be rewritten to remove the `ensure`. Interestingly enough, the ensure block is also how this is [implemented in Net::HTTP](https://ruby-doc.org/stdlib-2.4.2/libdoc/net/http/rdoc/Net/HTTP.html#method-i-start) itself.

## `retry`

The `retry` keyword allows retrying a piece of code in a block. Combined with a `rescue` block, we can use it to try again if we fail to open the connection, or if the API takes too long to respond.

To do that, we'll add a `read_timeout` to the `Net::HTTP.start` call which sets the timeout to 10 seconds. If a response to our request hasn't come in by then, it'll raise a `Net::ReadTimeout`.

We'll also match on `Errno::ECONNREFUSED` to handle the API being down completely, which would prevent us from opening the TCP connection. In that case, the `http` variable is `nil`.

The exception is rescued and `retry` is called to start the `begin` block again, which results in the code doing the same request until no timeout occurs. We'll reuse the `http` object which holds the connection if it already exists.

``` ruby
require "net/http"

http = nil
uri = URI("http://localhost:4567/")

begin
  unless http
    puts "Opening TCP connection..."
    http = Net::HTTP.start(uri.host, uri.port, read_timeout: 10)
  end
  puts "Executing HTTP request..."
  puts http.request_get(uri.path).body
rescue Errno::ECONNREFUSED, Net::ReadTimeout => e
  puts "Timeout (#{e}), retrying in 1 second..."
  sleep(1)
  retry
ensure
  if http
    puts "Closing the TCP connection..."
    http.finish
  end
end
```

Now, our request will retry every second until no `Net::ReadTimeout` is raised.

    $ ruby retry.rb
    Opening TCP connection...
    Executing HTTP request...
    Timeout (Net::ReadTimeout), retrying in 1 second...
    Executing HTTP request...
    Timeout (Net::ReadTimeout), retrying in 1 second...
    Executing HTTP request...
    Timeout (Net::ReadTimeout), retrying in 1 second...
    Executing HTTP request...
    ... (in an endless loop)


While that might make sure no exception is raised for any timeout ever, retry-hammering it like this certainly won't help to get that API back up again. This is problematic because this code will keep looping forever if the API remains unresponsive. Instead, we should spread our retries and give up after a while.

## Giving up: reraising exceptions using `raise`

When an exception is rescued, the raised exception object is passed to the `rescue` block. We can use that to extract data from the exception, like printing the message to the log, but we can also use it to reraise the exact same exception, with the same stack trace.

``` ruby
begin
  raise "Exception!"
rescue RuntimeError => e
  puts "Exception happened: #{e}"
  raise e
end
```

Since we have access to the exception object in the `rescue` block, we can log the error to the console or an error monitor. In fact, rescuing and reraising is exactly [how AppSignal's integrations track errors](https://github.com/appsignal/appsignal-ruby/blob/v2.6.1/lib/appsignal/rack/rails_instrumentation.rb#L31-L36).

*Note*: Ruby stores the last raised exception in a variable named `$!`, and the `raise` keyword will use it by default. Calling `raise` without any arguments will reraise the last exception.

In our library, we can use reraising to take the pressure of the API after a couple of retries. To do that, we'll keep track of the number of retries we've done in the `retries` variable.

Whenever a timeout happens, we'll increment the number and check if it's less than or equal to three, because we'd like to retry three retries at most. If so, we'll `retry`. If not, we'll `raise` to reraise the last exception.

``` ruby
require "net/http"

http = nil
uri = URI("http://localhost:4567/")
retries = 0

begin
  unless http
    puts "Opening TCP connection..."
    http = Net::HTTP.start(uri.host, uri.port, read_timeout: 1)
  end
  puts "Executing HTTP request..."
  puts http.request_get(uri.path).body
rescue Errno::ECONNREFUSED, Net::ReadTimeout => e
  if (retries += 1) <= 3
    puts "Timeout (#{e}), retrying in #{retries} second(s)..."
    sleep(retries)
    retry
  else
    raise
  end
ensure
  if http
    puts 'Closing the TCP connection...'
    http.finish
  end
end
```

By using the `retries` variable in the call to `sleep`, we can increase the wait time for every new attempt.

```
$ ruby reraise.rb
Opening TCP connection...
Executing HTTP request...
Timeout (Net::ReadTimeout), retrying in 1 second(s)...
Executing HTTP request...
Timeout (Net::ReadTimeout), retrying in 2 second(s)...
Executing HTTP request...
Timeout (Net::ReadTimeout), retrying in 3 second(s)...
Executing HTTP request...
Closing the TCP connection...
/lib/ruby/2.4.0/net/protocol.rb:176:in `rbuf_fill': Net::ReadTimeout (Net::ReadTimeout)
...
from reraise.rb:13:in `<main>'
```

Our request is retried three times before the code gives up and reraises the last error. We can then handle the error one level up, or crash our app if it can't finish its job without the API's response.

## A resilient web API client

By combining these methods, we've built a resilient web API client in about twenty lines of code. It'll retry requests if it's down or unresponsive, and we'll give up when it doesn't come back up again.

We hope you learned something new about handling exceptions and would love to know what you thought of this article (or any of the other ones in the [AppSignal Academy series](https://blog.appsignal.com/category/academy.html)). Please don't hesitate to [let us know](https://twitter.com/appsignal) what you think, or if you have any Ruby subjects you'd like to learn more about.