# Resolving Rails Runtime Errors: A Guide to Handling Unpersisted Changes with with_lock

## **Introduction**

In Ruby on Rails applications, handling database transactions efficiently is crucial to maintaining data integrity. One common issue developers face is dealing with runtime errors caused by unpersisted changes when using the `with_lock` method. This guide will help you understand and resolve these errors effectively.

## Environment

* Ruby 3.0.7
    
* Rails 6.1.7.7
    

## Error message

```ruby
RunTimeError: Locking a record with unpersisted changes is not supported. 
Use save to persist the changes, or reload to discard them explicitly.
```

*RunTimeError: Locking a record with unpersisted changes is not supported. Use save to persist the changes, or reload to discard them explicitly.*

## How to Reproduce

```ruby
class Invoice < ApplicationRecord
  before_save :sync_monetary_columns

  # Existing code...

  private
  
  def sync_monetary_columns
    return unless amount_changed?

    with_lock do 
      self.amount_decimal = amount.to_f
    end
  end
end
```

```ruby
invoice = Invoice.last
invoice.amount # 1000

invoice.amount = 500
invoice.save!
# RuntimeError: Locking a record with unpersisted changes is not supported. 
# Use `save` to persist the changes, or `reload` to discard them explicitly.
```

## Understanding how [`with_lock`](https://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html#method-i-with_lock) Works

`with_lock` is a Rails method that provides pessimistic locking, which is a database-level lock that prevents multiple processes from modifying the same record simultaneously. Here's how it works:

1. When `with_lock` is called, Rails:
    
    * Starts a database transaction
        
    * Reloads the record with `SELECT ... FOR UPDATE`
        
    * Executes your block of code
        
    * Commits the transaction
        
2. The `FOR UPDATE` lock means:
    
    * Other transactions must wait to read or modify this record
        
    * Prevents race conditions where multiple processes might try to update the same data
        
    * Lock is held until the transaction commits or rolls back
        

```ruby
def with_lock
  transaction do
    # Reloads the object and obtains lock:
    self.reload(lock: true)  # SELECT * FROM table WHERE id = 123 FOR UPDATE
    
    # Now yields to your block:
    yield
    
    # Transaction commits after your block completes
  end
end
```

## How to Fix This Error

To fix the runtime error caused by unpersisted changes, here’s my key implementation details:

1. Cache Values Before `with_lock`:
    
    * `with_lock` internally calls `reload`, which would clear any unpersisted changes. (Source code from [Rails](https://github.com/rails/rails/blob/cf6ff17e9a3c6c1139040b519a341f55f0be16cf/activerecord/lib/active_record/locking/pessimistic.rb#L69))
        
    * Caching preserves the values we need to modify.
        
2. Reload Only Persisted Records:
    
    * For persisted records: `with_lock` executes `reload(lock: true)` using the record's ID. When a record has unpersisted changes, this causes the RuntimeError.
        
    * For new records: No ID exists yet, so `reload` cannot trigger the error.
        

This solution ensures proper handling of both new and persisted records while maintaining data integrity during the locking process.

```ruby
class Invoice < ApplicationRecord
  before_save :sync_monetary_columns

  # Existing code...

  private
  
  def sync_monetary_columns
    return unless amount_changed?

    # Cache values 
    new_amount = amount.to_of

    reload if persisted?

    with_lock do 
      self.amount_decimal = new_amount
    end
  end
end
```

## Conclusion

By understanding the mechanics of `with_lock` and implementing the suggested solutions, you can effectively manage runtime errors related to unpersisted changes in your Rails applications. This approach not only resolves the issue but also enhances the robustness of your data handling processes.

---

## Reference

* [**Rails** `with_lock` Documentation](https://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html#method-i-with_lock)
