Sunset

Better Error Handling in Ruby with Rescue Else

Jared Carroll ·

The other day I came across some code that was making HTTP POST requests to a 3rd-party API. The API used 3 types of HTTP response codes:

  1. 200 (Ok)
  2. 422 (Client error)
  3. 500 (Server error)

Here is the code:

  def self.send_message(message)
    begin
      response = Net::HTTP.post_form(URI.parse(URL),
                                     :message => message)
      case response
      when Net::HTTPOK
        true   # success response
      when Net::HTTPClientError,
            Net::HTTPInternalServerError
        false  # non-success response
      end
    rescue Timeout::Error => error
      HoptoadNotifier.notify error
      false    # non-success response
    end
  end

The error handling in the above code seems to be casting too wide a net because the code in the legs of the case statement will never raise a Timeout::Error (Timeout::Error is one of several errors that can be raised from Net::HTTP.post_form; the above code has been simplified, it actually rescued several other errors as well).

So I refactored it to:

  def self.send_message(message)
    begin
      response = Net::HTTP.post_form(URI.parse(URL),
                                     :message => message)
    rescue Timeout::Error => error
      HoptoadNotifier.notify error
      false  # non-success response
    end
    case response
    when Net::HTTPOK
      true   # success response
    when Net::HTTPClientError,
          Net::HTTPInternalServerError
      false  # non-success response
    end
  end

It’s now very clear from the structure of the code where the Timeout::Error could be raised. However, if a Timeout::Error occurs now, the case statement will still be executed. A little research led me to the following from The Ruby Programming Language:

The else clause is an alternative to the rescue clauses; it is used if none of the rescue clauses are needed. That is, the code in an else clause is executed if the code in the body of the begin statement runs to completion without exceptions.

The use of an else clause is not particularly common in Ruby, but they can be stylistically useful to emphasize the difference between normal completion of a block of code and exceptional completion of a block of code.

Incorporating this into our code, leads to the following:

  def self.send_message(message)
    begin
      response = Net::HTTP.post_form(URI.parse(URL),
                                     :message => message)
    rescue Timeout::Error => error
      HoptoadNotifier.notify error
      false    # non-success response
    else
      case response
      when Net::HTTPOK
        true   # success response
      when Net::HTTPClientError,
           Net::HTTPInternalServerError
        false  # non-success response
      end
    end
  end

Now we have clear error handling and our case statement will not be executed when an error is raised.

To wrap up the refactoring, we can remove the begin and end blocks thanks to another feature of error handling in Ruby.

the body of a method definition is an implicit begin-end block

  def self.send_message(message)
    response = Net::HTTP.post_form(URI.parse(URL),
                                   :message => message)
  rescue Timeout::Error => error
    HoptoadNotifier.notify error
    false    # non-success response
  else
    case response
    when Net::HTTPOK
      true   # success response
    when Net::HTTPClientError,
         Net::HTTPInternalServerError
      false  # non-success response
    end
  end