Ruby Tips Part 3

By: Mat Sadler

Tags:

  • ruby

This is part 3 of a 5-part series on Ruby tips and tricks gleaned from our team’s pull requests over the last two years. Part 1 covers blocks and ranges, and part 2 deals with destructuring and type conversions.

Exceptions

Dealing with exceptions can be tricky, and it’s easy to dig a hole for yourself that’s much harder to get out of than in. There’s a couple of rules you can follow that might be a little more work to begin with but pay off in the long run, making your code easier to debug.

First off, don’t use the statement-modifier (postfix) rescue.

Here’s an example method that’ll return nil if there’s an error fetching an item from an array.

def safe_fetch(array, index)
  array[index] rescue nil
end

However we have no idea what error it’s hiding. The array could be nil, the index nil, the index could be a string, you could have made a typo on array, or any other error you could imagine.

This means we can’t work out the intention of the code from reading it, and the result of unexpected errors doesn’t show up here, but later on in the code when something gets passed that nil. At that point it can be very hard to work out that some unexpected error here is the source of that nil.

It’s going to save a lot of hassle if you use the standard form of rescue, either with the type of error we were expecting, or some logging.

def safe_fetch(array, index)
  array[index]
rescue NoMethodError
  nil
end
def safe_fetch(array, index)
  array[index]
rescue => e
  @logger.debug(e) if @logger
  nil
end

rescue => e is a shortcut for rescue StandardError => e, just like a bare rescue is a shortcut for rescue StandardError. This means the rescue will only rescue from StandardError or subclasses of StandardError. Generally you should follow Ruby’s example and only rescue from StandardError and its subclasses, never from Exception. Rescuing from Exception can mean you catch things you really shouldn’t, like SystemExit, the exception raised when your program has been asked to exit. If you rescue this you can end up with a program you can’t quit.

Likewise when raising errors you should only raise errors that are subclasses of StandardError, or you could end up with errors that blow though error handling they shouldn’t.

A good pattern to use in your own projects, particularly gems, is to define a general error for your whole project, and then have all others inherit from this.

module MyProject
  class Error < StandardError
  end

  class NotFoundError < Error
  end

  class PermissionError < Error
  end
end

This way you can easily rescue from any error in your project with rescue MyProject::Error, or you can be more specific.

A slightly nicer way to write this, giving you an easier to read list is to take advantage of the Class.new method. Class.new generates a new class inheriting from the one supplied as an argument. Ruby will also name a class created like this after the first constant it is assigned to.

module MyProject
  Error = Class.new(StandardError)
  NotFoundError = Class.new(Error)
  PermissionError = Class.new(Error)
end

One problem you may face with this approach is that not all the errors generated by your code are directly raised by your code. If for example you’re making HTTP requests you’re likely to get connection errors at some point. Now you could rescue these errors in your code, and raise new errors with types you control, but then you’re discarding valuable debugging information; the original class, message and backtrace.

A workaround for this takes advantage of Ruby allowing you to supply a class or module to rescue. rescue uses the case equality operator #=== to match its arguments against the exception. On Class this returns true if the argument is an instance of the class or instance of a subclass. On Module it returns true if the argument has the module included or extended in to it.

This means if you instead define the base error as a module, and include it into the more specific errors you get the same behaviour as before, but you can also ‘tag’ arbitrary errors raised by other bits of code you are using by extending them with the base error. These can then also be rescued by a rescue of the base error, but keep their original class, message and backtrace (you’ll no longer be able to raise the base error, but that’s ok, you should be raising the more specific errors).

module MyProject
  Error = Module.new

  class NotFoundError < StandardError
    include Error
  end

  class PermissionError < StandardError
    include Error
  end

  def get(path)
    response = do_get(path)
    raise NotFoundError, "#{path} not found" if response.code == "404"
    response.body
  rescue SocketError => e
    e.extend(Error)
    raise e
  end
end

...

begin
  get("/example")
rescue MyProject::Error => e
  e   #=> MyProject::NotFoundError or SocketError
end

Another use for this would be adapter classes for two disparate services (say database clients for example), mapping the various errors to the same categories, so your rescues still work after you switch out the client under the adapters.

This technique does however come with a drawback. The #extend call will invalidate Ruby’s global method cache, slowing your app for a moment each time it’s called. We will be getting more fine-grained method cache invalidation in Ruby 2.1, at which point there’s nothing to worry about. But even now this can be a very useful and powerful technique.

Modules

Modules serve a number of purposes in Ruby. Perhaps the most visible is as the name-spacing mechanism, and then there is their main intended use as ‘mixins’. Modules like Enumerable and Comparable are fantastic, easily allowing you to add complex behaviour to your classes by defining just a few methods and including the modules. Modules have some other uses too.

Sometimes you have a bunch of related methods that are much closer to functions, they take and input, return an output, and don’t do anything with self. Modules act as great way to group these methods together.

Here’s one from one of our apps (there are a couple more methods, but they’re a bit too specific to share).

module Geo
  module_function

  RADIUS_OF_THE_EARTH = 6371

  def distance((origin_lat, origin_long), (dest_lat, dest_long))
    return unless origin_lat && origin_long && dest_lat && dest_long

    sin_lats = Math.sin(rad(origin_lat)) * Math.sin(rad(dest_lat))
    cos_lats = Math.cos(rad(origin_lat)) * Math.cos(rad(dest_lat))
    cos_longs = Math.cos(rad(dest_long) - rad(origin_long))

    x = sin_lats + (cos_lats * cos_longs)
    x = [x, 1.0].min
    x = [x, -1.0].max

    Math.acos(x) * RADIUS_OF_THE_EARTH
  end

  def rad(degree)
    degree.to_f / (180 / Math::PI)
  end

  def degree(rad)
    rad.to_f * (180 / Math::PI)
  end

  def miles(km)
    km / 1.609344
  end

  def km(miles)
    miles * 1.609344
  end
end

The module_function declaration at the start of the module is a method visibility modifier, like public, private, or protected. It makes the method available directly on the module like a class method, and as a private instance method so the method is available un-prefixed when the module is included.

Geo.distance([51.47872, -0.610248], [51.5073346 , -0.1276831])   #=> 33.55959095208182

include Geo
distance([51.47872, -0.610248], [51.5073346 , -0.1276831])       #=> 33.55959095208182

A similar effect can be achieved switching module_fuction to extend self.

module Geo
  extend self
  ...
end

Here the instance methods of the module are copied as class methods. The difference is that you can use the other method visibility modifiers, making methods that are private on the module, or public on the instance (where they would have all been public on the module and private on the instance with module_fuction).

Modules also make for handy singleton objects with no need to mess about preventing more than one copy being instantiated or working out how to get ahold of the reference, it’s all built in to Ruby.

require "net/http"
module APIClient
  @http = Net:HTTP.new("example.com", 80)
  @user = "user"
  @pass = "pass"

  def self.get(path)
    request = Net::HTTP::Get.new(path)
    request.basic_auth(@user, @pass)
    @http.request
  end
end

response = APIClient.get("/api/examples")

Head on to Part 4.


About the Author

Mat Sadler