Search code examples
rubytwitterrubygemsrescue

Catching exceptions while using an external gem


I have written a program that utilizes an external ruby gem. As I am doing a lot of different actions with this, I want to be able to rescue and handle exceptions across the board, instead of implementing it each time I call a method.

What is the best way to do this?
Should I write my own method that simply calls the external gem and also rescues exceptions? Or is there another way to do something like "Whenever an exception of this type comes up anywhere in the program, handle it this way"?

I know that if I wrote the external gem code I could add error handling like that, but that is not feasible.


Solution

  • The basic answer to this is probably to wrap the class you're working with; Ruby allows for a lot of flexibility for doing this since it has method_missing and a pretty dynamic class environment. Here's an example (which may or may not be fatally flawed, but demonstrates the principle:

    # A Foo class that throws a nasty exception somewhere.
    class Foo
      class SpecialException < Exception; end
    
      def bar
        raise SpecialException.new("Barf!")
      end
    end
    
    # This will rescue all exceptions and optionally call a callback instead
    # of raising.
    class RescueAllTheThings
      def initialize(instance, callback=nil)
        @instance = instance
        @callback = callback
      end
    
      def method_missing(method, *args, &block)
        if @instance.respond_to? method
          begin
            @instance.send(method, *args, &block)
          rescue Exception => e
            @callback.call(e) if @callback
          end
        else
          super
        end
      end
    end
    
    # A normal non-wrapped Foo. Exceptions will propagate.
    raw_foo = Foo.new
    
    # We'll wrap it here with a rescue so that we don't exit when it raises.
    begin
      raw_foo.bar
    rescue Foo::SpecialException
      puts "Uncaught exception here! I would've exited without this local rescue!"
    end
    
    # Wrap the raw_foo instance with RescueAllTheThings, which will pass through
    # all method calls, but will rescue all exceptions and optionally call the
    # callback instead. Using lambda{} is a fancy way to create a temporary class
    # with a #call method that runs the block of code passed. This code is executed
    # in the context *here*, so local variables etc. are usable from wherever the
    # lambda is placed.
    safe_foo = RescueAllTheThings.new(raw_foo, lambda { |e| puts "Caught an exception: #{e.class}: #{e.message}" })
    
    # No need to rescue anything, it's all handled!
    safe_foo.bar
    
    puts "Look ma, I didn't exit!"
    

    Whether it makes sense to use a very generic version of a wrapper class, such as the RescueAllTheThings class above, or something more specific to the thing you're trying to wrap will depend a lot on the context and the specific issues you're looking to solve.