Understanding Ruby and Rails: Rescuable and rescue_from

This article targets Rails 2.3 Rails 3

The information contained in this page might not apply to different versions.

This is article is part of my series Understanding Ruby and Rails. Please see the table of contents for the series to view the list of all posts.

Last time I talked about the ActiveSupport Module#delegate method. Today, I want to introduce an other poweful ActiveSupport module: Rescuable, also known in the Rails ecosystem as rescue_from.

rescue_from and Rails

Starting from the release 2.0, Rails provides a clean way to rescue exceptions in a controller, mapping specific error classes to corresponding handlers.

Let's see an example. A call to ActiveRecord#find raises an ActiveRecord::RecordNotFound exception when the record passed as parameter doesn't exist. Assuming you want to display a nice 404 error page, you need to rescue the exception in each action where a find call is performed.

class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    render_404
  end

  def edit
    @post = Post.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    render_404
  end

  def destroy
    @post = Post.find(params[:id])
    @post.destroy
  rescue ActiveRecord::RecordNotFound
    render_404
  end
end

class UserController < ApplicationController
  def show
    @user = User.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    render_404
  end

  # ...
end

As you can see, this approach leads to lot of code duplication if you count the number of find calls for each action per model. The rescue_from method is exactly the solution we are looking for. Instead of catching the exception at action-level, we instruct the controller to rescue all the ActiveRecord::RecordNotFound errors and forward the exception to the proper handler.

class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, :with => :render_404
end

class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
  end

  def edit
    @post = Post.find(params[:id])
  end

  def destroy
    @post = Post.find(params[:id])
    @post.destroy
  end

end

class UserController < ApplicationController
  def show
    @user = User.find(params[:id])
  end

  # ...
end

The rescue_from method also accepts a block or a Proc. And if you need, you can also selectively rescue exceptions according to the error message or other properties.

rescue_from and Ruby

The rescue_from was born as a Rails feature but because it's packaged in the ActiveSupport::Rescuable module, you can easily reuse it elsewhere in your code to take advantage of the same clean and concise exception handling mechanism.

All you have to do is to require ActiveSupport library and include the ActiveSupport::Rescuable module in your class. If you are in a Rails project, ActiveSupport is already loaded. Then, add a rescue block and use the rescue_with_handler method to filter any error raised by the application.

class MyClass
  include ActiveSupport::Rescuable

  def method
    # ...
  rescue Exception => exception
    rescue_with_handler(exception) || raise
  end
end

The following is a simplified example extracted from RoboDomain. The Queue::Jobs::Base is the base class for all DelayedJob jobs. Each child class implements the perform method, as requested by DelayedJob. However, the base class provides an internal method called execute which wraps all executions and rescues from some known errors to prevent DelayedJob to re-schedule the failed task.

class Queue::Jobs::Base
  include ActiveSupport::Rescuable

  rescue_from ActiveRecord::RecordNotFound, :with => :known_error

  protected

    def execute(&block)
      yield
    rescue Exception => exception
      rescue_with_handler(exception) || raise
    end

    def known_error(exception)
      @error = exception
      Rails.logger.error "[JOBS] Exception #{exception.class}: #{exception.message}"
    end

end

class Queue::Jobs::FetchWhois < Queue::Jobs::Base

  rescue_from Hostname::NotLikeDomain, :with => :known_error
  # ActiveRecord::RecordNotFound already defined in parent class

  def initialize(hostname_id)
    @hostname_id = hostname_id
  end

  def perform
    execute do
      hostname = Hostname.find(@hostname_id)
    end
  end

end

As you can see, using the ActiveSupport::Rescuable module I don't need to clutter my code with multiple begin/rescue/raise statements.

A word of warning

Like any reusable pattern, ActiveSupport::Rescuable is not the ultimate and definitive solution for any piece of code where you need to rescue from an exception. Use it if you actually need it, don't try to force your code to fit this implementation.