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.