This blog is part of our Rails 6 series. Rails 6.0 was recently released.

Rails 6 fixes a bug where after_commit callbacks are called on failed update in a transaction block.

Let’s checkout the bug in Rails 5.2 and the fix in Rails 6.

Rails 5.2

Let’s define an after_commit callback in User model and try updating an invalid user object in a transaction block.

>> class User < ApplicationRecord
>>   validates :name, :email, presence: true
>>
>>   after_commit :show_success_message
>>
>>   private
>>
>>     def show_success_message
>>       p 'User has been successfully saved into the database.'
>>     end
>> end

=> :show_success_message

>> user = User.create(name: 'Jon Snow', email: 'jon@bigbinary.com')
begin transaction
User Create (0.8ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "Jon Snow"], ["email", "jon@bigbinary.com"], ["created_at", "2019-07-14 15:35:33.517694"], ["updated_at", "2019-07-14 15:35:33.517694"]]
commit transaction
"User has been successfully saved into the database."

=> #<User id: 1, name: "Jon Snow", email: "jon@bigbinary.com", created_at: "2019-07-14 15:35:33", updated_at: "2019-07-14 15:35:33">

>> User.transaction do
>>   user.email = nil
>>   p user.valid?
>>   user.save
>> end
begin transaction
false
commit transaction
"User has been successfully saved into the database."

=> false

As we can see here, that that the after_commit callback show_success_message was called even if object was never saved in the transaction.

Rails 6.0.0.rc1

Now, let’s try the same thing in Rails 6.

>> class User < ApplicationRecord
>>   validates :name, :email, presence: true
>>
>>   after_commit :show_success_message
>>
>>   private
>>
>>     def show_success_message
>>       p 'User has been successfully saved into the database.'
>>     end
>> end

=> :show_success_message

>> user = User.create(name: 'Jon Snow', email: 'jon@bigbinary.com')
SELECT sqlite_version(*)
begin transaction
User Create (1.0ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "Jon Snow"], ["email", "jon@bigbinary.com"], ["created_at", "2019-07-14 15:40:54.022045"], ["updated_at", "2019-07-14 15:40:54.022045"]]
commit transaction
"User has been successfully saved into the database."

=> #<User id: 1, name: "Jon Snow", email: "jon@bigbinary.com", created_at: "2019-07-14 15:40:54", updated_at: "2019-07-14 15:40:54">

>> User.transaction do
>>   user.email = nil
>>   p user.valid?
>>   user.save
>>   end
false

=> false

Now, we can see that after_commit callback was never called if the object was not saved.

Here is the relevant issue and the pull request.