Rails 5 doesn't halt callback chain if false is returned

Abhishek Jain

By Abhishek Jain

on February 13, 2016

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.

Stay up to date with our blogs. Sign up for our newsletter.

We write about Ruby on Rails, ReactJS, React Native, remote work,open source, engineering & design.