Our Forem code base has three primary exception handling strategies.
- Propagate up to Controller
- Propagate up to Application
Each of these strategies are valid, and things propagate from up from method to controller to application.
Below is an example of a function that handles all exceptions by writing them to the log. If any part of the
do_something raises an exception, we’ll capture it, and write it to the log.
Also whatever called
my_function will continue processing.
def my_function do_something rescue => e logger.error(e) end
Another variation is to capture a specific exception.
def my_function do_something rescue NoMethodError => e logger.error(e) end
In the above example, the code only handles
NoMethodError exceptions. If the
do_something method raised a
RuntimeError exception, our rescue would not handle that exception.
When specifying the exception, the rescue considers the inheritance of the exception object. The
rescue will handle any exception that is a descendant of the
In Rails, you can add handle exceptions at the controller level. Here’s the code you might see:
class UsersController rescue_from ActiveRecord::NotFoundError, with: :not_found def show @user = User.find(params[:id]) end private def not_found render "404.html", status: 404 end end
rescue_from method documentation for more details. Of particular note is the final line in the documentation: “Exceptions raised inside exception handlers are not propagated up.”
This means if you use a
rescue_from, and are looking at things in development, you won’t see the exception in the browser.
If you don’t use inline nor
rescue_from, your exceptions will bubble up to the application. And without any configuration, those visiting your site will see the default Rails exception page.
To handle exceptions at the application level you add them to the following to your application’s
In the below example all “Pundit::NotAuthorizedError” exceptions will call the
not_found method on the controller that handled the request.
config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :not_found
That’s the first piece of the configuration. The second part is to add another piece to the configuration; you want to set
You’ll often see the following in the
config.consider_all_requests_local = false
This means we are configuring Rails to look at the
config.action_dispatch.rescue_responses and call the corresponding methods. In other words, don’t show the ugly exceptions to our production users. There’s other configurations to ensure that we show a 500 error page, but that’s outside the scope of this post.
But in the development environment (e.g.
./config/environments/development.rb) that value will often be set to true. Which means, we are telling Rails to ignore the
config.action_dispatch.rescue_responses and will render the ugly, though often useful, exception in the browser.
When do I use which one? That depends. The further away from where you encounter the exception the more you have to consider.
First, if you can inline rescue, that’s great. But maybe don’t inline rescue every
My preference is to minimize the use of
rescue_from; it is the “always on”. And that means its hiding the call stack; something I find useful in my development work.
Awhile ago, I read Avdi Grimm’s Exceptional Ruby; I highly recommend picking it up and giving it a read to further understand the power and pitfalls of exceptions.