This blog is part of our Rails 5 series.
Before Rails 5, returning false from any before_ callback in ActiveModel or ActiveModel::Validations, ActiveRecord and ActiveSupport resulted in halting of callback chain.
1 2class Order < ActiveRecord::Base 3 4 before_save :set_eligibility_for_rebate 5 before_save :ensure_credit_card_is_on_file 6 7 def set_eligibility_for_rebate 8 self.eligibility_for_rebate ||= false 9 end 10 11 def ensure_credit_card_is_on_file 12 puts "check if credit card is on file" 13 end 14end 15 16Order.create! 17=> ActiveRecord::RecordNotSaved: ActiveRecord::RecordNotSaved 18
In this case the code is attempting to set the value of eligibility_for_rebate to false. However the side effect of the way Rails callbacks work is that the callback chain will be halted simply because one of the callbacks returned false.
Right now, to fix this we need to return true from before_ callbacks, so that callbacks are not halted.
Improvements in Rails 5
Rails 5 fixed this issue by adding throw(:abort) to explicitly halt callbacks.
Now, if any before_ callback returns false then callback chain is not halted.
1 2class Order < ActiveRecord::Base 3 4 before_save :set_eligibility_for_rebate 5 before_save :ensure_credit_card_is_on_file 6 7 def set_eligibility_for_rebate 8 self.eligibility_for_rebate ||= false 9 end 10 11 def ensure_credit_card_is_on_file 12 puts "check if credit card is on file" 13 end 14 15end 16 17Order.create! 18=> check if credit card is on file 19=> <Order id: 4, eligibility_for_rebate: false> 20
To explicitly halt the callback chain, we need to use throw(:abort).
1 2class Order < ActiveRecord::Base 3 4 before_save :set_eligibility_for_rebate 5 before_save :ensure_credit_card_is_on_file 6 7 def set_eligibility_for_rebate 8 self.eligibility_for_rebate ||= false 9 throw(:abort) 10 end 11 12 def ensure_credit_card_is_on_file 13 puts "check if credit card is on file" 14 end 15 16end 17 18Order.create! 19=> ActiveRecord::RecordNotSaved: Failed to save the record 20
Opting out of this behavior
The new Rails 5 application comes up with initializer named callback_terminator.rb.
ActiveSupport.halt_callback_chains_on_return_false = false
By default the value is to set to false.
We can turn off this default behavior by changing this configuration to true. However then Rails shows deprecation warning when false is returned from callback.
1 2ActiveSupport.halt_callback_chains_on_return_false = true 3 4class Order < ApplicationRecord 5 before_save :set_eligibility_for_rebate 6 before_save :ensure_credit_card_is_on_file 7 8 def set_eligibility_for_rebate 9 self.eligibility_for_rebate ||= false 10 end 11 12 def ensure_credit_card_is_on_file 13 puts "check if credit card is on file" 14 end 15end 16 17=> DEPRECATION WARNING: Returning `false` in Active Record and Active Model callbacks will not implicitly halt a callback chain in the next release of Rails. To explicitly halt the callback chain, please use `throw :abort` instead. 18ActiveRecord::RecordNotSaved: Failed to save the record 19
How older applications will work with this change?
The initializer configuration will be present only in newly generated Rails 5 apps.
If you are upgrading from an older version of Rails, you can add this initializer yourself to enable this change for entire application.
This is a welcome change in Rails 5 which will help prevent accidental halting of the callbacks.