Peter Marklund

Peter Marklund's Home

Fri April 24, 2009
Programming

Rails Gotcha: ActiveRecord##Base.valid?, errors.empty?, and before_validation Callbacks

The ActiveRecord::Callbacks module (that depends on ActiveSupport::Callbacks) defines a multitude of before and after callbacks for the lifecycle of your ActiveRecord objects. Similar to how before filters in controllers work, if a before_validation callback method returns false, the save process will be aborted. Here is what the API documentation says:

# If the returning value of a +before_validation+ callback can be evaluated to +false+,
# the process will be aborted and Base#save< will return +false+.
# If Base#save! is called it will raise a ActiveRecord::RecordInvalid exception.
# Nothing will be appended to the errors object.

# If a before_* callback returns +false+, all the later callbacks and the associated
# action are cancelled. If an after_* callback returns
# +false+, all the later callbacks are cancelled. Callbacks are generally run in the 
# order they are defined, with the exception of callbacks
# defined as methods on the model, which are called last.

What this means is that if a before_validation callback returns false, then save and valid? will both return false but errors.empty? will return true. Yes, you read correctly, there are no validation errors but the record is not valid. Also, note that the valid? method defined in the ActiveRecord::Base class will never even be invoked.

Comments

Marko said over 5 years ago:

Just into this issue, thanks for noting it.

--------------------------------------------------------------------------------

krukid said over 4 years ago:

A handled exception in one of before_validation callbacks also seems to be interpreted as a failed validation, ex.:

def before_validation
unsafe_call
rescue
rescue_proc
end

and the ar.save doesn't make it to the validate method.
A workaround would be:

def before_validation
unsafe_call
rescue
rescue_proc
ensure
return true
end

Also, any errors you add manually (self.errors.add) in the callback are dropped if you return true.

--------------------------------------------------------------------------------

Nick said over 4 years ago:

I handle this by using

errors.add_to_base( "My reason for returning false" )

before I return false

--------------------------------------------------------------------------------