Our Forem code base has three primary exception handling strategies.
- Inline
- 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.
Handling Strategies
Inline
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 NoMethodError
class.
Propagate Up to Controller
In Ruby on Rails (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
See the 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.
Propagate Up to Application Handling
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 ./config/application.rb
.
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 config.consider_all_requests_local
.
You’ll often see the following in the ./config/environments/production.rb
.
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.
Conclusion
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 ActiveRecord::RecordNotFound
exception?
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.