Nobody likes to see exceptions when running an application; it means something went wrong and you should have been on top of it. But with that, often times I find people will write code to prevent exceptions rather than just allowing them to occur and then using it in their favor.
Take this bit of ruby code in a rails controller as an example:
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
end
end
That’s pretty solid, but what happens if someone passes in an ID that does not exist? Well in rails’ production mode, it would return a 404 error.
Makes sense! That article doesn’t exist, so it can’t be found. I have dabbled with a little SEO and usability and just a 404 isn’t cutting it for me. I think we can redirect our user back on a right track. For one, we know the user is trying to read an article, so why not redirect the user to a list of all the articles and give them a little warning?
Solution One
First check that the article exists, and if not, redirect the user to the index page.
def show
if Article.exists?(params[:id])
@article = Article.find(params[:id])
else
flash[:error] = 'That article does not exist'
redirect_to :action => 'index'
end
end
While this works, this requires two queries to our database; one to check it’s existence and one to load it. I think there is a better way, and its secret is to go with the flow of the code.
Solution Two
One thing
ActiveRecord::Base#find does do is raise an exception when it cannot find anything, an ActiveRecord::RecordNotFound exception. We can use this bit of knowledge to re-write that method to something a little simpler by trusting it to throw an exception and then for us to rescue it.
def show @article = Article.find(params[:id]) rescue ActiveRecord::RecordNotFound flash[:error] = 'That article does not exist' redirect_to :action => 'index' end
One call to the database and fewer lines of code (not a lot, but still). This time, that nasty little error has been used in our favor; but one mustn’t get lazy when using rescue statements.
Don’t Get Lazy
Notice that I used the constant ActiveRecord::RecordNotFound with the statement. Sure, I could have just written rescue, but that hurts us in two ways. First, it becomes less readable without it. Having the RecordNotFound is pretty obvious as to the reason we are executing that statement. Secondly, it can introduce unwanted side effects.
def show @article = Article.find(params[:id]) some_illegal_operation rescue flash[:error] = 'That article does not exist' redirect_to :action => 'index' end
Whenever that method is called, it will always redirect the user to the index page with that error message. Because we’ve rescued all errors, we’re not getting any notice about how our some_illegal_operation has caused us grief. This makes debugging and management harder.
The lesson being, don’t be lazy in those rescues. Try to avoid a blanket rescue statement if you can.
December 3rd, 2006 at 11:27 pm #Maxim Kulkin
There is also another solution:
def show @article = Article.find_by_id(params[:id]) unless @article flash[:error] = 'That article does not exist' redirect_to :action => 'index' end endAnd don’t forget about verify method that could be used to make sure that user passed non-empty id param to that action.
Although I would prefer ot put such rescue clauses in separate place (e.g. rescue_action_in_public).
December 4th, 2006 at 5:26 am #Adam
Forgot about that way. Thanks.
I was not familiar with
verifyhowever. That is a pretty useful method as well, but since I’ve moved to using the RESTful stuff in rails 1.2RC1, That kind of stuff already happens for me, with the IDs and whathave you. Plus,verifyis only a first step, it does not verify that your data exists and something like therescueor your conditional statement is still necessary.rescue_action_in_publicis a neat method; however, I often reserve that method for Exception LoggerJanuary 2nd, 2007 at 8:31 pm #rick
I think using rescue_action_in_public is a good, DRY way to handle this. I also use just rescue_action to handle common ActiveRecord::RecordInvalid exceptions by rendering either the ‘new’ or ‘edit’ template.