Understanding Callback Execution Order and Avoiding Infinite Loops in Ruby on Rails

Urvashi NigamUrvashi Nigam
2 min read

When working with callbacks in Ruby on Rails, understanding their execution order and how to avoid pitfalls like infinite loops is crucial. Let's delve into an example to illustrate these concepts! ๐Ÿš€
The Problem: Callbacks and Infinite Loops ๐Ÿ”„

Consider a User model with the following callbacks:

class User < ApplicationRecord
  after_update_commit :update_user_name, if: -> { saved_change_to_status? && accepted? }
  after_update_commit :update_user_age, if: -> { saved_change_to_status? && accepted? }

  def update_user_name
    update(name: "Aman")  # Use update_columns to avoid callbacks
    Rails.logger.debug "User name updated to: #{self.name}"
  end

  def update_user_age
    update(age: "22")  # Use update_columns to avoid callbacks
    Rails.logger.debug "User age updated to: #{self.age}"
  end

  enum status: {
    pending: 'pending',
    accepted: 'accepted',
    past_due_date: 'past_due_date',
    cancelled: 'cancelled',
    processing: 'processing',
    analyzing: 'analyzing'
  }.freeze
end

Why Only the First Callback Runs?

When updating status from pending to accepted, typically only the first callback executes due to:

  1. Execution Order: Callbacks run in the sequence defined within the model.
    For instance:
# Rearranging the sequence
class User < ApplicationRecord
  after_update_commit :update_user_name, if: -> { saved_change_to_status? && accepted? }
  after_update_commit :update_user_age, if: -> { saved_change_to_status? && accepted? }

  def update_user_name
    update(name: "Aman")  # Use update_columns to avoid callbacks
    Rails.logger.debug "User name updated to: #{self.name}"
  end

  def update_user_age
    update(age: "22")  # Use update_columns to avoid callbacks
    Rails.logger.debug "User age updated to: #{self.age}"
  end

  enum status: {
    pending: 'pending',
    accepted: 'accepted',
    past_due_date: 'past_due_date',
    cancelled: 'cancelled',
    processing: 'processing',
    analyzing: 'analyzing'
  }.freeze
end

Now, you might observe: - output
User name updated to: Aman

How to Overcome This Challenge? ๐Ÿ’ก
To avoid triggering additional callbacks and maintain control over updates, consider using update_columns:

class User < ApplicationRecord
  after_update_commit :update_user_age, if: -> { saved_change_to_status? && accepted? }
  after_update_commit :update_user_name, if: -> { saved_change_to_status? && accepted? }

  def update_user_name
    update_columns(name: "Aman")  # Bypass callbacks
    Rails.logger.debug "User name updated to: #{name}"
  end

  def update_user_age
    update_columns(age: "22")  # Bypass callbacks
    Rails.logger.debug "User age updated to: #{age}"
  end

  enum status: {
    pending: 'pending',
    accepted: 'accepted',
    past_due_date: 'past_due_date',
    cancelled: 'cancelled',
    processing: 'processing',
    analyzing: 'analyzing'
  }.freeze
end

Summary

Execution Order: Callbacks are executed sequentially as defined in the model.

  • Conditional Checks: Conditions (saved_change_to_status? && accepted?) ensure callbacks run only when specific criteria are met.

  • Callbacks and Data Integrity: Utilize methods like update_columns judiciously to prevent unintended callback chains and maintain data integrity.

    Additional Note ๐Ÿ“

    This approach with update_columns is suitable when updating attributes that don't require updated_at timestamps or validation callbacks. It's a powerful tool in your Rails arsenal, allowing fine-grained control over updates without triggering cascading callbacks.
    By exploring these concepts, we deepen our understanding of Rails development practices and foster a community of shared knowledge. Happy coding! ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป

12
Subscribe to my newsletter

Read articles from Urvashi Nigam directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Urvashi Nigam
Urvashi Nigam