we-promise/sure · error · ActiveRecord::RecordInvalid
Replacement tag cannot be the same as the tag being…
Error message
Replacement tag cannot be the same as the tag being destroyed
What it means
Raised as ActiveRecord::RecordInvalid by Tag#replace_and_destroy! (app/models/tag.rb:18) when the replacement argument is the very same Tag instance being destroyed. The method exists to move all taggings onto another tag before destroying this one; passing self would make the update_all point taggings at a record that is about to be deleted, silently orphaning every transaction tag. The guard runs inside a transaction, so nothing is modified when it fires.
Solutions
- Fix the caller: resolve the replacement from params[:replacement_id] (not params[:id]) and verify it differs from the destroyed tag's id before calling replace_and_destroy!.
- In the UI, filter the tag being destroyed out of the replacement-tag picker so the state is unreachable.
- If destroying without merging is intended, pass nil: tag.replace_and_destroy!(nil) destroys the tag and its taggings.
- If you hit this in a controller, render a 422 with errors.full_messages instead of letting RecordInvalid bubble up as a 500.
Example fix
// before
tag.replace_and_destroy!(replacement) # replacement == tag -> RecordInvalid
// after
replacement = family.tags.find(params[:replacement_id]) if params[:replacement_id].present?
if replacement&.id == tag.id
return render json: { error: 'Replacement tag must be different' }, status: :unprocessable_entity
end
tag.replace_and_destroy!(replacement) Defensive patterns
Strategy: validation
Validate before calling
def valid_replacement?(tag, replacement) replacement.nil? || replacement.id != tag.id end tag.replace_and_destroy!(replacement) if valid_replacement?(tag, replacement)
Type guard
replacement.nil? || (replacement.is_a?(Tag) && replacement.family_id == tag.family_id && replacement.id != tag.id)
Try / catch
begin
tag.replace_and_destroy!(replacement)
rescue ActiveRecord::RecordInvalid => e
# errors[:base] carries the message; surface as 422
render json: { error: e.message }, status: :unprocessable_entity
end Prevention
- Exclude the tag being destroyed from the replacement picker in the UI.
- Always resolve the replacement by a separate param (replacement_id), never reuse params[:id].
- Pass nil explicitly when no merge is intended — nil is a supported 'just destroy' path.
When it happens
Trigger: Calling tag.replace_and_destroy!(tag) directly, or UI/controller code that resolves a 'replacement tag' select-box and passes back the original tag (e.g. the form defaults to the tag being deleted, or an ID mix-up sends params[:id] as params[:replacement_id]). Also fires when both resolve to the same record via find(params[:replacement_id]) with the same id value.
Common situations: A tag-deletion dialog where the user picks the tag being deleted as its own replacement; frontend that echoes the destroyed tag's id back as the replacement; test fixtures that reuse the same tag object for both arguments.
Related errors
- Anthropic Model is required when a custom Base URL is set.
- must be a whole number ≥ .
- # must be an ISO 8601 date
- # must be an ISO 8601 date
- max_row_count_exceeded
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/5e0d603065d01b3b.
Report an issue: GitHub.
Appendix: source
Thrown at app/models/tag.rb:18
class Tag < ApplicationRecord
belongs_to :family
has_many :taggings, dependent: :destroy
has_many :transactions, through: :taggings, source: :taggable, source_type: "Transaction"
has_many :import_mappings, as: :mappable, dependent: :destroy, class_name: "Import::Mapping"
validates :name, presence: true, uniqueness: { scope: :family }
validates :color, format: { with: /\A#[0-9A-Fa-f]{6}\z/ }, allow_nil: true
scope :alphabetically, -> { order(:name, :id) }
COLORS = %w[#e99537 #4da568 #6471eb #db5a54 #df4e92 #c44fe9 #eb5429 #61c9ea #805dee #6ad28a]
UNCATEGORIZED_COLOR = "#737373"
def replace_and_destroy!(replacement)
transaction do
raise ActiveRecord::RecordInvalid, "Replacement tag cannot be the same as the tag being destroyed" if replacement == self
if replacement
taggings.update_all tag_id: replacement.id
end
destroy!
end
end
end
View on GitHub (pinned to e69894adb9)