ruby 64 lines · 4 tabs

Safer Deletion with dependent: :restrict_with_error

Shared by codesnips Jan 2026
4 tabs
class Category < ApplicationRecord
  has_many :products, dependent: :restrict_with_error
  has_many :subcategories,
           class_name: "Category",
           foreign_key: :parent_id,
           dependent: :restrict_with_error

  belongs_to :parent, class_name: "Category", optional: true

  validates :name, presence: true, uniqueness: { scope: :parent_id }

  def deletable?
    products.none? && subcategories.none?
  end
end
4 files · ruby Explain with highlit

This snippet demonstrates a defensive pattern for handling deletions in a Rails application where destroying a parent record could strand its children. Instead of silently cascading a destroy (which can wipe out large trees of data) or blindly deleting and later hitting a database foreign-key violation, the models declare dependent: :restrict_with_error so ActiveRecord refuses the deletion and attaches a friendly error message when related rows still exist.

In Category model, the association has_many :products, dependent: :restrict_with_error tells ActiveRecord to check for dependent Product rows before running the destroy. If any exist, the callback halts the transaction and adds an error to the record's base attribute rather than raising an exception. This differs from :restrict_with_exception, which raises ActiveRecord::DeleteRestrictionError — the _with_error variant is friendlier for user-facing flows because it flows through the normal validation error channel. Note that the check counts existing children, so it does not depend on records loaded in memory.

In Product model, a second layer is shown: dependent: :restrict_with_error on line_items means a product cannot be removed while it appears on any historical order, protecting order integrity. This keeps the domain rule close to the data.

The CategoriesController shows how the pattern pays off in a controller. Because destroy returns false instead of raising, the action can branch cleanly: on success it redirects, and on failure it reads @category.errors.full_messages to render an explanation. There is no rescue block cluttering the flow.

The schema migration reinforces the app-level guard with a real database-level foreign_key constraint. This belt-and-suspenders approach matters: dependent: :restrict_with_error only protects deletions going through ActiveRecord callbacks, while a database foreign key stops orphaning from raw SQL, delete_all, or concurrent races. The trade-off is that restrict-style deletions require callers to clean up children first, so it suits reference data (categories, tags, users with audit trails) more than ephemeral records. When cascading truly is desired, dependent: :destroy remains the right tool.


Related snips

Share this code

Here's the card — post it anywhere.

Safer Deletion with dependent: :restrict_with_error — share card
Link copied