How *_was Method Works in ActiveRecord

Being able to detect changes in model attributes is often essential for building reactive, state-aware systems in Rails. Whether you’re logging updates, triggering side effects, or enforcing validations, it helps to know what a value used to be before it was changed.

Rails makes this straightforward with the *_was method pattern—like.

In this post, we’ll take a closer look at how *_was works in Active Record, how and when to use these methods effectively, and what to watch out for.

What is *_was in Active Record?

*_was method is part of Active Record’s dirty tracking system. It returns the value of the given attribute before any in-memory changes were made. Essentially, this method allows you to retrieve the previous value of an attribute during the current lifecycle of the object—before it’s saved to the database.

Rails dynamically creates helper methods for each attribute in your model. These methods rely on the core method attribute_was, which works like this:

1
2
3
def attribute_was(attr)
  attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
end

If the attribute was changed in memory, attribute_was returns its previous value from the changed_attributes hash. If it wasn’t changed, it simply returns the current value.

View in Rails source

A Simple Example: Tracking Name Changes in Active Record

Let’s start with a simple example using a name attribute.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Account < ApplicationRecord
  # ...
end

account = Account.create(name: 'John')
# => account.name is now 'John'

account.name = 'Jane'
# => account.name is now 'Jane'

account.name_was
# => "John" (the value before the change)

account.save
# After saving, the dirty state is cleared.

account.name_was
# => "Jane" (if no further changes were made after saving)

This is particularly useful in before_update callbacks when you want to react to a change and compare old vs. new values.

A More Realistic Example: Responding to Status Transitions in Active Record

The power of *_was methods really shines when you want to trigger logic based on specific state transitions in your models. Here’s an example from an e-commerce application where we want to react to order status changes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Order < ApplicationRecord
  before_update :handle_status_transition

  private

  def handle_status_transition
    return unless status_changed?

    old_status = status_was
    new_status = status

    case [old_status, new_status]
    when ['pending', 'shipped']
      notify_shipped
    when ['shipped', 'delivered']
      notify_delivery
    end
  end

  def notify_shipped
    # Send shipping confirmation email
    OrderMailer.shipping_confirmation(self).deliver_later
  end

  def notify_delivery
    # Send delivery notification
    OrderMailer.delivery_notification(self).deliver_later
  end
end

Important Notes

While *_was methods are convenient, there are a few things to keep in mind when using them:

1. Only Works Before Save

After saving, the dirty state is reset. That means status_was no longer returns the pre-change value—it returns the current one.

1
2
3
4
5
user.status = 'active'
user.status_was # => 'inactive'

user.save
user.status_was # => 'active'

2. If the Attribute Didn’t Change, *_was == *_current

If no changes were made to the attribute, *_was will be equal to the current value:

1
2
user = User.find(1)
user.status_was == user.status # => true

The *_was value only differs from the current value if the attribute was changed in the current transaction (i.e., since the last save or load).

3. Doesn’t Persist History

These methods track in-memory changes only. If you need full audit logging or historical state tracking across multiple saves, consider using gems.

When to Use *_was Methods in Active Record

Use it in:

Avoid using it for: