norman/friendly_id · warning
is reserved
Error message
is reserved
What it means
FriendlyId::Reserved (lib/friendly_id/reserved.rb:37) adds `validates_exclusion_of :friendly_id` against `friendly_id_config.reserved_words`, so a save fails with "is reserved" on the :friendly_id attribute whenever the resolved slug equals a reserved word. The Reserved module is active by default (`FriendlyId.defaults` does `config.use :reserved`), and the Rails generator's initializer reserves route-colliding words such as "new", "edit", "index", "admin", "login" (lib/friendly_id/initializer.rb:19). It is a validation failure, not an exception: `record.save` returns false and `record.valid?` is false.
Source
Thrown at lib/friendly_id/reserved.rb:37
# class Person < ActiveRecord::Base
# extend FriendlyId
# friendly_id :name, use: :slugged
#
# after_validation :move_friendly_id_error_to_name
#
# def move_friendly_id_error_to_name
# errors.add :name, *errors.delete(:friendly_id) if errors[:friendly_id].present?
# end
# end
#
# @guide end
module Reserved
# When included, this module adds configuration options to the model class's
# friendly_id_config.
def self.included(model_class)
model_class.class_eval do
friendly_id_config.class.send :include, Reserved::Configuration
validates_exclusion_of :friendly_id, in: ->(_) {
friendly_id_config.reserved_words || []
}
end
end
# This module adds the `:reserved_words` configuration option to
# {FriendlyId::Configuration FriendlyId::Configuration}.
module Configuration
attr_accessor :reserved_words
attr_accessor :treat_reserved_as_conflict
end
end
end
View on GitHub (pinned to 4699c9791d)
Solutions
- Set `config.treat_reserved_as_conflict = true` in the FriendlyId initializer so the slug generator treats reserved words as taken and appends a sequence ("new-2") instead of failing validation.
- Provide fallback candidates so validation can step past the reserved word: `friendly_id :name, use: [:slugged, :cached_candidates], candidates: [:name, [:name, :id]]`.
- Trim `reserved_words` to words that actually collide with your routes — e.g. drop "admin" if it is not a top-level slug in your app.
- When assigning friendly_id manually, check the value against `Model.friendly_id_config.reserved_words` (or suffix it) before assignment.
- Surface the error to users: add an `after_validation :move_friendly_id_error_to_name` callback that re-adds errors[:friendly_id] onto the name attribute, as shown in reserved.rb's guide.
Example fix
# before (config/initializers/friendly_id.rb) FriendlyId.defaults do |config| config.reserved_words = %w[new edit index session login logout users admin] end # Post.create!(title: "New") # => ActiveRecord::RecordInvalid: validation failed: Friendly ID is reserved # after FriendlyId.defaults do |config| config.reserved_words = %w[new edit index] config.treat_reserved_as_conflict = true end # Post.create!(title: "New").slug # => "new-2"
Defensive patterns
Strategy: validation
Validate before calling
slug = record.title.parameterize
reserved = Post.friendly_id_config.reserved_words || []
slug = "#{slug}-2" if reserved.include?(slug) # or pick another candidate
# or, after the fact:
# record.valid? && record.errors[:friendly_id].include?("is reserved") Prevention
- Set `treat_reserved_as_conflict = true` so the generator sequences past reserved words instead of failing validation.
- Keep the reserved_words list minimal and reviewed — only words that collide with real routes.
- Cover reserved-word titles with model specs so the failure is caught in CI, not production.
- Move the :friendly_id error onto a real attribute for form display via after_validation.
- Never hardcode reserved slugs in seeds or fixtures.
When it happens
Trigger: Creating or updating a record whose slug resolves to a reserved word — e.g. with the generated initializer `Post.create(title: "New")` produces slug "new", which is in the reserved list, so `save`/`valid?` adds "is reserved" to errors[:friendly_id]. Also triggered by assigning `friendly_id` directly to a reserved value, and typically happens when `treat_reserved_as_conflict` is not enabled, because the slugged generator then proposes the reserved slug instead of sequencing past it. Any model using FriendlyId inherits the Reserved validation via the default defaults block even if the author never asked for it.
Common situations: Scaffolded CRUD forms show nothing when this fires, because the error sits on :friendly_id which has no form field (the guide in reserved.rb shows moving it to :name with an after_validation hook); single-word titles like "admin" or "new"; teams editing the initializer's reserved list and forgetting dependent models; seed files and test fixtures that hardcode reserved slugs; keeping the default reserved list even though those words don't collide with the app's actual routes.
AI-assisted analysis of norman/friendly_id@4699c9791d (2026-08-21).
Data as JSON: /api/errors/b7b2c6fbc7fcb673.
Report an issue: GitHub.