WideFix tech post

Rescue exceptions DSL for plain Ruby objects with Rails

At some point, you can start to think about how it would be great to have in your pure Ruby objects of your Rails application the same DSL as Rails’ controllers have to rescue exceptions coming from actions with the rescue_from help. If you are interested in this topic then keep reading and I will show how it’s easy to implement it from scratch.

TL;DR: use ActiveSupport::Rescuable.

Introduction

It’s not a secret to everyone that it’s too easy to catch all targeted exceptions raised in any Rails controller. Just use rescue_from in a base controller like this:

class ApplicationController < ActionController::Base
  rescue_from CanCan::AccessDenied do
    redirect_to root_path, alert: "You don't have access to this page."
  end
end

And that’s it. Whenever the CanCan::AccessDenied is raised in some action of inherited controllers from the ApplicationController the Ruby interpreter jumps into this block, defined with the rescue_from and then it’s executed. As a result, a user will be redirected to the main page with an alert and the original exception is suppressed. There are different ways how to use the rescue_from method, but this post is not about this, so you can find all variations here.

The main idea of this article is to show you how to add such functionality into some other type of objects of your Rails application. Say, you have a bunch of service objects that reside at the same hierarchy branch. And you want to catch some general exception in the base class of these service objects easily and, you think, that it would be good to have this functionality in the descendants.

Extend custom objects with Rescuable

Actually, it’s not a problem at all. All you have to do is just to include the ActiveSupport::Rescuable module into a base class and wrap the method responsible for the execution of some work that could raise an exception which you would like to catch later with the rescue_from.

To not be wordy I will just provide the following code snippet which demonstrates the main idea:

class BaseService
  include ActiveSupport::Rescuable

  class FieldIsNilError < StandardError; end

  rescue_from FieldIsNilError do |exception|
    puts "Field is empty: #{exception.class} - #{exception.message}"
  end

  def call
    call_with_rescue { useful_yield }
  end

  private

  def call_with_rescue
    yield
  rescue => e
    rescue_with_handler(e) || raise(e)
  end

  def useful_yield
    fail NotImplementedError
  end
end

class LoginUserService < BaseService
  class InvalidEmailError < StandardError; end
  class EmptyEmailError < StandardError; end
  class SecurityError < StandardError; end

  rescue_from InvalidEmailError, EmptyEmailError do |exception|
    puts "Logged invalid login attempt: #{exception.class} - #{exception.message}"
  end

  def initialize(email: nil)
    @email = email
  end

  private

  def useful_yield
    case @email
    when 'invalid'
      fail InvalidEmailError, 'email is invalid'
    when ''
      fail EmptyEmailError, 'email is empty'
    when 'kill -9'
      fail SecurityError, 'throw out'
    when nil
      fail FieldIsNilError, 'email is nil'
    else
      puts 'login ok'
    end
  end
end

LoginUserService.new(email: 'invalid').call
# => Logged invalid login attempt: LoginUserService::InvalidEmailError - email is invalid
LoginUserService.new(email: '').call
# => Logged invalid login attempt: LoginUserService::EmptyEmailError - email is empty
LoginUserService.new(email: '[email protected]').call
# => login ok
LoginUserService.new(email: nil).call
# => Field is empty: BaseService::FieldIsNilError - email is nil
LoginUserService.new(email: 'kill -9').call
# => throw out (LoginUserService::SecurityError)

The main trick here is in the include ActiveSupport::Rescuable. It provides us with the rescue_from method, defined on the class level. And also it adds the rescue_with_handler method which tries to find a handler for a raised exception and call this handler if it’s found. The handler is defined with the rescue_from in a service object - it’s just a block. We use this in the call_with_rescue method, which wraps that method that does a real job and can raise an exception at some point (this is the useful_yield method). And this exception can be caught with the rescue_from and some useful work can be done suppressing the error. Or it will be raised up and an end user will observe it in case if we don’t have a defined rescue handler for this exception.

Now let’s experiment with this code. Just place the code above in a test.rb file located in a Rails application and execute it with the rails runner using following command: rails runner test.rb. You will have an output similar to this one:

Logged invalid login attempt: LoginUserService::InvalidEmailError - email is invalid
Logged invalid login attempt: LoginUserService::EmptyEmailError - email is empty
login ok
Field is empty: BaseService::FieldIsNilError - email is nil
test.rb:49:in `useful_yield': throw out (LoginUserService::SecurityError)

If you feel uncomfortable at this point you can change this code and rerun it with the rails runner, it’s rather easy, as you see. Or just comment on this post. I would be glad to hear your response and questions.

Conclusion

Try to understand how interesting things are implemented, that you would like to have in your code, and consume the implementation. But note, that sometimes it’s easier to implement some things yourself from scratch, there are may be many reasons for this: code quality, lack of functionality that can’t be extended easily and so on. Every case should be analyzed and a correct decision should be taken. But it’s not about this case. The ActiveSupport::Rescuable does its work and does it gracefully.

Are you seeking assistance with Ruby on Rails development?

Read also