This article was originally posted on the globaldev blog, they have kindly allowed me to repost it here. If you’re looking for a Ruby job in London you should check out their jobs page.
This is part 3 of a 5-part series on Ruby tips and tricks gleaned from the Global Personals team’s pull requests over the last two years. Part 1 covers blocks and ranges, and part 2 deals with destructuring and type conversions.
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 rescue
s 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 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.