Skip to content

Commit

Permalink
Raise instead of returning false in AR callbacks
Browse files Browse the repository at this point in the history
We never check whether a `#destroy` fails in our controllers:

    def destroy
      article = Article.find(params[:id])
      article.destroy
      redirect_to articles_url
    end

Because of this history, it would cause confusion and potential bugs if
we introduced a normal `before_destroy` into any of our models. Not only
will we need to check the result of `#destroy` for any controller using
that model, but we'd also need to check the return value for any
controller using models that associate with that model.

Since callbacks are used for validated database objects and are expected
to succeed, we should raise on failure. This will roll back the
transaction and notify the exception handler (e.g.  Airbrake).

This is true of all callbacks, not just `before_destroy`.
  • Loading branch information
mike-burns committed Jul 8, 2014
1 parent bae91a2 commit 8ddc91b
Showing 1 changed file with 2 additions and 0 deletions.
2 changes: 2 additions & 0 deletions best-practices/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ Rails
* Don't change a migration after it has been merged into master if the desired
change can be solved with another migration.
* Don't reference a model class directly from a view.
* Don't return false from `ActiveModel` callbacks, but instead raise an
exception.
* Don't use instance variables in partials. Pass local variables to partials
from view templates.
* Don't use SQL or SQL fragments (`where('inviter_id IS NOT NULL')`) outside of
Expand Down

8 comments on commit 8ddc91b

@MikeSilvis
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mike-burns this sounds to me like more of an issue of not verifying if the object was destroyed first. A validation could provide helpful feedback to a user as to why you can't delete a specific object, yet they are now going to see a 500 error.

@mike-burns
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MikeSilvis what's an example error message that would be helpful for the user in this case?

@MikeSilvis
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mike-burns what if the business posts an arbitrary constraint such that you can't destroy an article after it has been live for 24 hours.

class Article < ActiveRecord::Base
  validates :not_older_than_24_hours, :on => :destroy
  def not_older_than_24_hours
    article.created_at < 1.day.ago
  end
end

@jferris
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validations are different than callbacks. During callbacks, it doesn't make sense to add error messages, because they would just be cleared on the next valid? or save run.

Can you add validations which will run on destroy? If so, that may be a good approach for some situations, but it still wouldn't provide a reason to return false in a callback.

@MikeSilvis
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes valid. My key complaint was looking at the following code snippet:

    def destroy
      article = Article.find(params[:id])
      article.destroy
      redirect_to articles_url
    end

And the philosophy that you should not do:

    def destroy
      article = Article.find(params[:id])
      if article.destroy
        redirect_to articles_url
      else
        redirect_to article_path(article)
      end
    end

Furthermore if you really just wanted an error to be thrown you could just do a article.destroy!

@mike-burns
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MikeSilvis You are correct, that is how the #destroy action should be written. However, that is not how it is traditionally written, it is not how it was written in the past, and it is not how the Rails scaffold generator produces it today. This guideline is for those cases.

@nickrivadeneira
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mike-burns This guideline seems fine to me, but just for the sake of discussion, that Rails scaffold doesn't work this way and that controllers haven't previously been written this way don't feel like compelling arguments for a guideline.

It makes more sense to have a guideline around how to best destroy a record than a guideline on how to adapt to the wrong way of destroying a record.

@mike-burns
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I submitted this I was looking at thoughtbot's existing project repos -- all of which used a pattern that would cause issue -- many of which were legacy apps written by other teams. We needed a way to handle #destroy in those safely and without fanfare.

I call #destroy! when on greenfield projects. Legacy projects, especially from other teams, cause the solution presented in this PR to still be relevant.

So I guess: pragmaticism vs idealism.

Please sign in to comment.