Rails 6 fixes after_commit callback invocation bug

Amit Choudhary

Amit Choudhary

February 25, 2020

This blog is part of our  Rails 6 series.

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.

1>> class User < ApplicationRecord
2>>   validates :name, :email, presence: true
3>>
4>>   after_commit :show_success_message
5>>
6>>   private
7>>
8>>     def show_success_message
9>>       p 'User has been successfully saved into the database.'
10>>     end
11>> end
12
13=> :show_success_message
14
15>> user = User.create(name: 'Jon Snow', email: '[email protected]')
16begin transaction
17User Create (0.8ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "Jon Snow"], ["email", "[email protected]"], ["created_at", "2019-07-14 15:35:33.517694"], ["updated_at", "2019-07-14 15:35:33.517694"]]
18commit transaction
19"User has been successfully saved into the database."
20
21=> #<User id: 1, name: "Jon Snow", email: "[email protected]", created_at: "2019-07-14 15:35:33", updated_at: "2019-07-14 15:35:33">
22
23>> User.transaction do
24>>   user.email = nil
25>>   p user.valid?
26>>   user.save
27>> end
28begin transaction
29false
30commit transaction
31"User has been successfully saved into the database."
32
33=> 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.

1>> class User < ApplicationRecord
2>>   validates :name, :email, presence: true
3>>
4>>   after_commit :show_success_message
5>>
6>>   private
7>>
8>>     def show_success_message
9>>       p 'User has been successfully saved into the database.'
10>>     end
11>> end
12
13=> :show_success_message
14
15>> user = User.create(name: 'Jon Snow', email: '[email protected]')
16SELECT sqlite_version(*)
17begin transaction
18User Create (1.0ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "Jon Snow"], ["email", "[email protected]"], ["created_at", "2019-07-14 15:40:54.022045"], ["updated_at", "2019-07-14 15:40:54.022045"]]
19commit transaction
20"User has been successfully saved into the database."
21
22=> #<User id: 1, name: "Jon Snow", email: "[email protected]", created_at: "2019-07-14 15:40:54", updated_at: "2019-07-14 15:40:54">
23
24>> User.transaction do
25>>   user.email = nil
26>>   p user.valid?
27>>   user.save
28>>   end
29false
30
31=> 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.

If this blog was helpful, check out our full blog archive.

Stay up to date with our blogs.

Subscribe to receive email notifications for new blog posts.