
A Ruby module is a container for methods and constants that can’t be instantiated. As a namespace it prevents name clashes; as a mixin it adds behavior to a class: include adds instance methods, extend adds class methods, and prepend inserts the module before the class so its methods run first. Rails’ ActiveSupport::Concern builds on the same mechanism.
That is the short version. The rest covers namespaces and constant lookup, the three mixin verbs with the ancestors chain that explains them, and mixin hooks. It also covers the standard library’s Comparable and Enumerable, refinements, ActiveSupport::Concern on Rails 8.1, and the seven module errors developers paste into a search box. Every snippet and every error message here ran on Ruby 3.4.10 and Rails 8.1.3.1.
What Are Modules in Ruby?
A module starts with the module keyword instead of class and, like a class, groups methods and constants under one name. The two are close relatives: Class is a subclass of Module, so every class is a module with the extra ability to create objects.
Class.is_a?(Module) # => true
Class.superclass # => ModuleWhat a plain module lacks is exactly what makes it useful as a mixin:
- It can’t be instantiated:
InvoiceCreator.newraisesundefined method 'new' for module InvoiceCreator (NoMethodError). - It can’t be inherited from:
class Invoice < InvoiceCreatorraisessuperclass must be an instance of Class (given an instance of Module) (TypeError). - It gets mixed into classes instead, with
include,extend, orprepend.
Modules are not flat, though. A module can include other modules and has an ancestors list of its own, which matters once several mixins stack up.
Here is a module that holds a constant and both kinds of method a module can provide:
module InvoiceCreator
TAX_FEE = 0.5
def self.generate
puts "Don't worry! I'll generate the invoice for you at #{TAX_FEE}%"
end
def invoice_total
puts "I'll return the invoice total"
1000
end
endself.generate is a module method. It lives on the module object itself, so you call it without mixing anything in, which is the usual shape of a small service module:
InvoiceCreator.generate$ ruby invoice_creator.rb
Don't worry! I'll generate the invoice for you at 0.5%
invoice_total is an instance method, and a module has no instances. To call it, a class has to include the module:
class Invoice
include InvoiceCreator
def calculate_tax
total = invoice_total # included method from our module
tax = total * InvoiceCreator::TAX_FEE
puts "This is the invoice tax: #{tax}"
end
end
Invoice.new.calculate_tax$ ruby invoice.rb
I'll return the invoice total
This is the invoice tax: 500.0
Every instance method of InvoiceCreator is now an instance method of Invoice. The constant travels too: Invoice::TAX_FEE returns 0.5, and inside calculate_tax a bare TAX_FEE would have resolved as well; constants through mixins explains why.
Utility modules often want a method available both ways: as TextUtil.slugify from anywhere, and as a bare slugify inside a class that includes TextUtil. module_function does that in one line. Every method defined after it becomes a module method and a private instance method:
module TextUtil
module_function
def slugify(text)
text.downcase.strip.gsub(/[^a-z0-9]+/, "-").delete_suffix("-")
end
end
class Post
include TextUtil
def slug
slugify("Mixins and Modules!")
end
end
TextUtil.slugify("Mixins and Modules!") # => "mixins-and-modules"
Post.new.slug # => "mixins-and-modules"
Post.new.slugify("x")
# => private method 'slugify' called for an instance of Post (NoMethodError)The private copy keeps slugify out of Post’s public interface, so it never shows up on an object that has nothing to do with text.
Modules as Namespaces
The second thing a module does is hold a name. Suppose the application needs two invoice creators, one for customers and one for suppliers. Two top-level InvoiceCreator modules would be one module: Ruby reopens a module that already exists, and the second def self.generate replaces the first without a word. Nesting each one inside a module of its own gives each a separate name:
module Customer
module InvoiceCreator
def self.generate
puts "Don't worry! I'll generate the customer invoice for you"
end
end
end
module Supplier
module InvoiceCreator
def self.generate
puts "Don't worry! I'll generate the supplier invoice for you"
end
end
end
Customer::InvoiceCreator.generate
Supplier::InvoiceCreator.generate$ ruby namespaces.rb
Don't worry! I'll generate the customer invoice for you
Don't worry! I'll generate the supplier invoice for you
:: walks into a namespace from the outside. From the inside, Ruby resolves an unqualified constant through Module.nesting first: the modules and classes lexically enclosing the current line, innermost first. Then it checks the ancestors of the innermost one. The nesting depends on how the definition was written, not on the resulting name:
module Billing
TAX = 0.2
class Receipt
def nesting
Module.nesting
end
def tax
TAX
end
end
end
class Billing::Invoice
def nesting
Module.nesting
end
def tax
TAX
end
end
Billing::Receipt.new.nesting # => [Billing::Receipt, Billing]
Billing::Receipt.new.tax # => 0.2
Billing::Invoice.new.nesting # => [Billing::Invoice]
Billing::Invoice.new.tax
# => uninitialized constant Billing::Invoice::TAX (NameError)The compact class Billing::Invoice form skips Billing in the nesting, so a bare TAX has nowhere to resolve. Nest the definitions, or write Billing::TAX; the NameError entry has the full message.
Modules as Mixins: include, extend, and prepend
Ruby has single inheritance: a class has exactly one superclass. Behavior that several unrelated classes share can’t come from a common parent, so it comes from modules mixed in, which is composition over inheritance as the language ships it. Three verbs do the mixing, and the way to tell them apart is ancestors: the ordered list of classes and modules Ruby searches when you call a method. One module serves all three examples:
module Greeting
def greet
"hello from #{self.class}"
end
endinclude: Instance Methods and the Ancestors Chain
include inserts the module into the ancestors chain directly after the class. Instances gain the module’s instance methods, and the module shows up in type checks:
class Person
include Greeting
end
Person.ancestors # => [Person, Greeting, Object, Kernel, BasicObject]
Person.new.greet # => "hello from Person"
Person.include?(Greeting) # => true
Person.new.is_a?(Greeting) # => trueWhen greet is called on a Person, Ruby walks that list from the left: Person has no greet, Greeting does. Kernel near the end is a module too, included in Object; puts and require come from there.
extend: Methods on the Object Itself
extend adds a module’s methods to one object rather than to a class’s instances. It is include applied to that object’s singleton class, which is why the class’s own ancestors don’t change:
class Company
extend Greeting
end
Company.ancestors # => [Company, Object, Kernel, BasicObject]
Company.singleton_class.ancestors # => [#<Class:Company>, Greeting, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
Company.greet # => "hello from Class"Company.greet reports hello from Class because inside the method self is the Company class object, whose class is Class. Extending a class is how a module provides class methods. Extending any other object gives that single object the methods and leaves its siblings alone:
report = Object.new
report.extend(Greeting)
report.greet # => "hello from Object"
Object.new.respond_to?(:greet) # => falseprepend: Running Before the Class
prepend places the module before the class in ancestors. Ruby finds the module’s method first, even when the class defines a method with the same name:
class Robot
prepend Greeting
def greet
"beep"
end
end
Robot.ancestors # => [Greeting, Robot, Object, Kernel, BasicObject]
Robot.new.greet # => "hello from Robot"Robot#greet never runs. The useful version of this is a module that calls super, which wraps the original method instead of replacing it:
module Loud
def greet
super.upcase + "!"
end
end
class Shouter
prepend Loud
def greet
"hi"
end
end
Shouter.ancestors # => [Loud, Shouter, Object, Kernel, BasicObject]
Shouter.new.greet # => "HI!"Because Shouter#greet still exists and super reaches it, this is the safe way to change a method you don’t own, and the standard alternative to reopening a class. Responsible Monkeypatching in Ruby weighs prepend against the other options for third-party code.
Method Lookup, super, and Several Mixins
Every lookup follows one rule: walk ancestors left to right and stop at the first match. That rule settles what happens when two included modules define the same method. Each include inserts its module directly after the class, so the module included last sits closest to the class and wins. The earlier one is still there, one step further along, and super reaches it:
module First
def name
"first"
end
end
module Second
def name
"second, then #{super}"
end
end
class Both
include First
include Second
end
Both.ancestors # => [Both, Second, First, Object, Kernel, BasicObject]
Both.new.name # => "second, then first"The methods don’t overwrite each other; they shadow each other, and super peels the layers back. A method the class defines itself beats every included module, and can still fall through to them:
module Fallback
def greet
"fallback"
end
end
class Overrider
include Fallback
def greet
"class first, then #{super}"
end
end
Overrider.new.greet # => "class first, then fallback"Including a module that is already in the chain changes nothing:
Person.include(Greeting)
Person.ancestors # => [Person, Greeting, Object, Kernel, BasicObject]Modules stack the same way. A module can include other modules, and a class that includes the outer module gets the whole chain:
module InvoiceCalculator
def calculate_items_total
puts "imagine some math here"
end
end
module InvoiceRenderer
def generate_invoice_pdf
puts "imagine that we are using some PDF generation magic here"
end
end
module InvoiceSender
def send_invoice
puts "imagine your favorite mail service being used here"
end
end
module Customer
module InvoiceCreator
include InvoiceCalculator
include InvoiceRenderer
include InvoiceSender
def generate_invoice
calculate_items_total # from InvoiceCalculator
generate_invoice_pdf # from InvoiceRenderer
send_invoice # from InvoiceSender
end
end
end
class Invoice
include Customer::InvoiceCreator
end
Invoice.new.generate_invoice$ ruby invoice_chain.rb
imagine some math here
imagine that we are using some PDF generation magic here
imagine your favorite mail service being used here
Invoice includes one module and ends up with four in its chain, in the order the lookup rule predicts:
Customer::InvoiceCreator.ancestors # => [Customer::InvoiceCreator, InvoiceSender, InvoiceRenderer, InvoiceCalculator]
Invoice.ancestors # => [Invoice, Customer::InvoiceCreator, InvoiceSender, InvoiceRenderer, InvoiceCalculator, Object, Kernel, BasicObject]Composition by mixin is one option. When the shared behavior belongs to a separate object, forwarding calls to that object is the other, and How to Delegate Methods in Ruby covers it.
Constants Through Mixins
Constants ride along with include. A constant defined in a module is reachable through the including class, from outside with :: and from inside with a bare name. Constant lookup walks the ancestors after it has checked the lexical nesting:
module Limits
MAX_ITEMS = 10
end
class Cart
include Limits
def full?(count)
count >= MAX_ITEMS
end
end
Cart::MAX_ITEMS # => 10
Cart.new.full?(10) # => true
Cart.constants # => [:MAX_ITEMS]
Cart.constants(false) # => []constants(false) shows the difference: Cart can see MAX_ITEMS, but doesn’t own it.
Mixin Hooks: included, extended, and prepended
Ruby calls a hook on the module each time it is mixed in: self.included(base), self.extended(base), and self.prepended(base), where base is the class or object receiving the module. The classic use is providing class methods alongside instance methods. The module supplies instance methods as usual and, in included, extends the host with a nested ClassMethods module:
module Trackable
def self.included(base)
puts "Trackable included in #{base}"
base.extend(ClassMethods)
end
module ClassMethods
def tracked?
true
end
end
def track
"tracking #{self.class}"
end
end
class Widget
include Trackable
end
p Widget.tracked?
p Widget.new.track$ ruby trackable.rb
Trackable included in Widget
true
"tracking Widget"
One include line gives Widget an instance method and a class method. An included hook is also where a mixin can generate methods on the host class; define_method is the tool, and our metaprogramming guide shows how it builds methods from a list of names. Rails packages the hook-plus-ClassMethods pattern as ActiveSupport::Concern.
Standard Library Mixins: Comparable and Enumerable
Ruby’s own library relies on the same mechanism: implement one method, include a module, receive a whole interface.
Comparable asks for <=>, which returns -1, 0, 1, or nil when the two objects can’t be compared. In return it provides exactly seven methods: <, <=, ==, >, >=, between?, and clamp:
class Version
include Comparable
attr_reader :major, :minor
def initialize(string)
@major, @minor = string.split(".").map(&:to_i)
end
def <=>(other)
return nil unless other.is_a?(Version)
[major, minor] <=> [other.major, other.minor]
end
def to_s
"#{major}.#{minor}"
end
end
Comparable.instance_methods(false).sort # => [:<, :<=, :==, :>, :>=, :between?, :clamp]
Version.new("3.4") > Version.new("3.3") # => true
Version.new("3.2").between?(Version.new("3.0"), Version.new("3.4")) # => true
Version.new("4.0").clamp(Version.new("3.0"), Version.new("3.4")).to_s # => "3.4"
[Version.new("3.4"), Version.new("3.0"), Version.new("3.2")].sort.map(&:to_s) # => ["3.0", "3.2", "3.4"]
[Version.new("3.4"), Version.new("3.0")].max.to_s # => "3.4"
Version.new("3.4") < "3.3"
# => comparison of Version with String failed (ArgumentError)The nil return is what produces that last ArgumentError instead of a confusing NoMethodError from inside <=>. Leave <=> out entirely and every comparison fails the same way, against the class itself:
class Version
include Comparable
def initialize(string)
@parts = string.split(".").map(&:to_i)
end
end
Version.new("3.4") < Version.new("3.3")$ ruby version_missing.rb
version_missing.rb:9:in 'Comparable#<': comparison of Version with Version failed (ArgumentError)
Version.new("3.4") < Version.new("3.3")
^^^^^^^^^^^^^^^^^^
from version_missing.rb:9:in '<main>'
Enumerable asks for each and gives back 61 methods on Ruby 3.4, map, select, sort_by, each_slice, and lazy among them:
class Playlist
include Enumerable
def initialize(*tracks)
@tracks = tracks
end
def each(&block)
@tracks.each(&block)
end
end
playlist = Playlist.new("Intro", "Verse", "Chorus")
playlist.map(&:upcase) # => ["INTRO", "VERSE", "CHORUS"]
playlist.select { |t| t.size > 5 } # => ["Chorus"]
playlist.sort_by(&:size) # => ["Intro", "Verse", "Chorus"]
playlist.include?("Verse") # => true
playlist.each_slice(2).to_a # => [["Intro", "Verse"], ["Chorus"]]
playlist.lazy.map(&:upcase).first(2) # => ["INTRO", "VERSE"]
Enumerable.instance_methods(false).size # => 61
Playlist.instance_method(:map).owner # => EnumerableForget each and the failure surfaces inside the module: Enumerable#map calls each on your object and raises undefined method 'each' for an instance of Playlist (NoMethodError). Our guide to Enumerable and Enumerator goes through what the module builds on top of each.
Refinements: Mixins with a Scope
include and prepend change a class for the whole program. A refinement changes it for one lexical scope. You define it with refine inside a module and switch it on with using; before that line, and in every other file, "hi".shout raises undefined method 'shout' for an instance of String (NoMethodError):
module StringShout
refine String do
def shout
upcase + "!"
end
end
end
StringShout.refinements # => [#<refinement:String@StringShout>]
StringShout.refinements.first.target # => String
using StringShout
"hi".shout # => "HI!"Refinements are the tool when a change must not leak, for example a helper on String that only one gem or one directory should see. The monkeypatching post weighs them against prepend for changes that should apply everywhere.
ActiveSupport::Concern in Rails
A Rails concern is a module that extends ActiveSupport::Concern. The extension adds three things to what a plain module already does: an included block that runs with the including class as self, a class_methods block that replaces the hand-written ClassMethods module, and dependency resolution between concerns. Every example in this section ran inside a fresh rails new app on Rails 8.1.3.1 with activesupport 8.1.3.1 and zeitwerk 2.8.3.
included do and class_methods do
A concern for models that need a URL slug:
# app/models/concerns/sluggable.rb
module Sluggable
extend ActiveSupport::Concern
included do
before_validation :set_slug
validates :slug, presence: true
end
class_methods do
def find_by_slug!(slug)
find_by!(slug: slug)
end
end
def set_slug
self.slug ||= title.to_s.parameterize
end
end# app/models/article.rb
class Article < ApplicationRecord
include Sluggable
endbefore_validation and validates are class-level calls, so they can’t sit in the module body, where self is Sluggable. Inside included do, self is Article, and the callback and the validator land on the model. class_methods do collects class methods into Sluggable::ClassMethods and extends the including class with it:
Sluggable::ClassMethods.instance_methods(false) # => [:find_by_slug!]
Article.validators.map { |v| [v.class.name, v.attributes] } # => [["ActiveRecord::Validations::PresenceValidator", [:slug]]]
Article.new.valid? # => false
article = Article.create!(title: "Mixins and Modules")
article.slug # => "mixins-and-modules"
Article.find_by_slug!("mixins-and-modules") == article # => trueA prepended do block exists as well, for concerns that a class mixes in with prepend.
Concerns That Depend on Other Concerns
A concern can include another concern, and the outer one does not need to know what the inner one sets up:
# app/models/concerns/publishable.rb
module Publishable
extend ActiveSupport::Concern
include Sluggable
included do
scope :published, -> { where.not(published_at: nil) }
end
class_methods do
def publish_all!
find_each(&:publish!)
end
end
def publish!
update!(published_at: Time.current)
end
def published?
published_at.present?
end
end# app/models/article.rb
class Article < ApplicationRecord
include Publishable
endActiveSupport::Concern sees that Publishable is itself a concern and holds back Sluggable’s included block until a real class includes Publishable. Both blocks then run against Article, and both class_methods blocks extend it:
Article.ancestors.first(4).map(&:name) # => ["Article", "Publishable", "Sluggable", "Article::GeneratedAssociationMethods"]
Article.respond_to?(:find_by_slug!) # => true
Article.respond_to?(:publish_all!) # => true
Publishable.respond_to?(:find_by_slug!) # => falseThe same two modules written without ActiveSupport::Concern, using self.included(base) and base.before_validation, fail at load time with undefined method 'before_validation' for module PlainPublishable (NoMethodError): the hook ran against the intermediate module, which has no callbacks. That failure is the reason the concern module exists; the error entry has the code.
Where Concerns Live: app/models/concerns and Zeitwerk
Rails autoloads with Zeitwerk, and the concern directories are autoload roots, not namespaces. The first eight roots on a fresh 8.1 app:
Rails.autoloaders.main.dirs.map { |dir| dir.delete_prefix("#{Rails.root}/") }.first(8)
# => ["lib", "app/controllers", "app/controllers/concerns", "app/helpers", "app/jobs", "app/mailers", "app/models", "app/models/concerns"]So app/models/concerns/taggable.rb must define Taggable, and app/controllers/concerns/authenticatable.rb must define Authenticatable. The pre-Zeitwerk habit of writing Concerns::Taggable raises uninitialized constant Concerns (NameError), and a file that defines module Concerns::Taggable fails the same way when Zeitwerk loads it. Controller concerns work like model concerns:
# app/controllers/concerns/authenticatable.rb
module Authenticatable
extend ActiveSupport::Concern
included do
before_action :require_login
end
private
def require_login
head :unauthorized unless session[:user_id]
end
end# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include Authenticatable
endApplicationController.ancestors.first(2) # => [ApplicationController, Authenticatable]
ApplicationController._process_action_callbacks.map { |callback| [callback.kind, callback.filter] }.last # => [:before, :require_login]The 2021 version of this post kept its module in lib/modules/invoice_creator.rb. On Rails 8.1 that no longer loads. The generated config/application.rb contains config.autoload_lib(ignore: %w[assets tasks]), which makes lib an autoload root, so Zeitwerk expects that file to define Modules::InvoiceCreator. A model with include InvoiceCreator raises uninitialized constant Invoice::InvoiceCreator (NameError), and bin/rails zeitwerk:check names the real problem:
$ bin/rails zeitwerk:check
Hold on, I am eager loading the application.
expected file lib/modules/invoice_creator.rb to define constant Modules::InvoiceCreator, but didn't
Move the file to lib/invoice_creator.rb, or into app/models/concerns/ if it is a model mixin, and Invoice.new.calculate_tax prints its two lines again. Whenever a constant name and a file path disagree, run bin/rails zeitwerk:check first; it reports every mismatch in one pass. The autoloading guide documents the naming rules, and the ActiveSupport::Concern API page documents the blocks.
Rails Is Built From Concerns
Open a model’s ancestors and the pattern is everywhere. On a default 8.1 app, ActiveRecord::Base has 74 ancestors, and 53 of them extend ActiveSupport::Concern:
ancestors = ActiveRecord::Base.ancestors
ancestors.size # => 74
ancestors.count { |mod| mod.singleton_class.include?(ActiveSupport::Concern) } # => 53
ancestors.first # => ActionText::Encryption
ancestors.values_at(25, 47, 57, 68) # => [ActiveRecord::Callbacks, ActiveRecord::Validations, ActiveRecord::Persistence, Object]
Article.instance_method(:valid?).owner # => ActiveRecord::Validations
Article.instance_method(:valid?).super_method.owner # => ActiveModel::Validationsvalid? is defined in ActiveRecord::Validations, which calls super into ActiveModel::Validations further along the chain: the same lookup rule as First and Second. The first entry, ActionText::Encryption, comes before ActiveRecord::Base itself because it is prepended, so prepend is in production Rails too. For a concern that only one model uses, Module#concerning defines it inline: concerning :Archiving do ... end inside Article creates Article::Archiving, a real concern in the ancestors, without a separate file.
Module and Mixin Errors: Causes and Fixes
Every message is copied from Ruby 3.4.10 or Rails 8.1.3.1, so you can match it against your terminal character for character. Ruby 3.4 prints straight quotes and Class#method frames; older versions print backticks and bare method names for the same errors.
wrong argument type Class (expected Module) (TypeError)
include_class.rb:5:in 'Module#include': wrong argument type Class (expected Module) (TypeError)Cause: include, extend, and prepend accept modules only. Passing a class, usually because a helper was written with class when it should have been module, raises at the include line:
class Timestamps
end
class Invoice
include Timestamps
endFix: Change class Timestamps to module Timestamps. If the class is meant to be a parent, inherit from it instead: class Invoice < Timestamps. The mirror-image mistake, inheriting from a module, raises superclass must be an instance of Class (given an instance of Module) (TypeError); a module needs include.
undefined method 'tags' for an instance of Invoice (NoMethodError)
extend_mixup.rb:11:in '<main>': undefined method 'tags' for an instance of Invoice (NoMethodError)Cause: The module was mixed in with the wrong verb. extend put tags on the Invoice class object, but the call is on an instance:
module Taggable
def tags
[]
end
end
class Invoice
extend Taggable
end
Invoice.new.tagsThe twin message, undefined method 'tags' for class Invoice (NoMethodError), means the opposite: the module was included, and the method is being called on the class.
Fix: include for methods on instances, extend for methods on the class. When a module needs both, use an included hook with base.extend(ClassMethods), or class_methods do in a Rails concern.
uninitialized constant Billing::Invoice::TAX (NameError)
billing.rb:7:in 'Billing::Invoice#tax': uninitialized constant Billing::Invoice::TAX (NameError)Cause: The class was defined with the compact class Billing::Invoice form, so Billing is not in Module.nesting and a bare TAX can’t be found:
module Billing
TAX = 0.2
end
class Billing::Invoice
def tax
TAX
end
end
Billing::Invoice.new.taxFix: Nest the definition (module Billing around class Invoice) so the namespace is in scope, or qualify the constant as Billing::TAX.
cyclic include detected (ArgumentError)
cyclic.rb:9:in 'Module#append_features': cyclic include detected (ArgumentError)Cause: Two modules include each other, so the ancestors chain would loop. The error is raised at the second include, in whichever file loads last:
module Auditable
end
module Trackable
include Auditable
end
module Auditable
include Trackable
endA module that includes itself raises the same message.
Fix: Break the cycle. Move the shared methods into a third module both can include, or let the class include both modules side by side and rely on the lookup order.
Cannot define multiple 'included' blocks for a Concern (ActiveSupport::Concern::MultipleIncludedBlocks)
/usr/local/bundle/gems/activesupport-8.1.3.1/lib/active_support/concern.rb:162:in 'ActiveSupport::Concern#included': Cannot define multiple 'included' blocks for a Concern (ActiveSupport::Concern::MultipleIncludedBlocks)
from /work/demo/app/models/concerns/broken.rb:9:in '<module:Broken>'Cause: A concern calls included do twice, often after a merge that brought two versions of the same block together. ActiveSupport::Concern stores one block per module and refuses a second:
# app/models/concerns/broken.rb
module Broken
extend ActiveSupport::Concern
included do
validates :title, presence: true
end
included do
validates :slug, presence: true
end
endFix: Merge the two into one included do block. The same rule applies to prepended do, which raises ActiveSupport::Concern::MultiplePrependBlocks.
expected file app/models/concerns/taggable.rb to define constant Taggable, but didn't (Zeitwerk::NameError)
(demo):1:in '<main>': expected file /work/demo/app/models/concerns/taggable.rb to define constant Taggable, but didn't (Zeitwerk::NameError)Cause: Zeitwerk derives the expected constant from the file path, and the file defines something else. The two usual reasons are a misspelled constant, as in this file, and the pre-Zeitwerk Concerns:: prefix (module Concerns::Taggable), which fails with uninitialized constant Concerns (NameError) because app/models/concerns is an autoload root, not a namespace:
# app/models/concerns/taggable.rb
module Tagable
extend ActiveSupport::Concern
endFix: Make the constant match the path: app/models/concerns/taggable.rb defines Taggable, and app/models/billing/tax.rb defines Billing::Tax. The console form carries the absolute path; bin/rails zeitwerk:check prints the relative form in this heading and lists every mismatch at once.
undefined method 'before_validation' for module PlainPublishable (NoMethodError)
app/models/concerns/plain_sluggable.rb:4:in 'PlainSluggable.included': undefined method 'before_validation' for module PlainPublishable (NoMethodError)
base.before_validation :set_slug
^^^^^^^^^^^^^^^^^^
from app/models/concerns/plain_publishable.rb:3:in 'Module#include'Cause: One plain-Ruby module includes another, and the inner module’s self.included hook calls model methods on base. That hook fires at the moment of inclusion, when base is the outer module, not the model:
# app/models/concerns/plain_sluggable.rb
module PlainSluggable
def self.included(base)
base.before_validation :set_slug
end
def set_slug
self.slug ||= title.to_s.parameterize
end
end# app/models/concerns/plain_publishable.rb
module PlainPublishable
include PlainSluggable
def publish!
update!(published_at: Time.current)
end
endFix: Add extend ActiveSupport::Concern to both modules and move the base.before_validation call into an included do block. The concern module holds the inner block back until a class includes the outer module, so it runs against the model.
Wrapping Up
When a mixin does something unexpected, print ancestors before reading any more code: the chain says which method wins, where super goes, and whether a module was included, prepended, or extended onto the singleton class. On Rails, bin/rails zeitwerk:check answers the other half, whether the file and the constant agree. Between those two commands, most module mysteries take a minute to clear up.
P.S. If you’d like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!
Frequently asked questions
- What is the difference between include and extend in Ruby?
- include adds a module’s methods as instance methods and places the module in the class’s ancestors, so every instance gains them. extend adds the same methods to one object’s singleton class; on a class, that means class methods. Use include for instance behavior and extend for class-level helpers.
- When should I use prepend instead of include in Ruby?
- Use prepend when the module must run before the class’s own method of the same name. Ruby inserts a prepended module ahead of the class in ancestors, so its method is found first and can call super to reach the original. Prepend is the safe way to wrap existing methods.
- What is ActiveSupport::Concern for in Rails?
- ActiveSupport::Concern gives a module an included block that runs in the context of the including class, a class_methods block for class-level methods, and dependency resolution: when one concern includes another, both apply to the final class. It removes the base.extend and self.included boilerplate that plain Ruby mixins need.
- How do I organize modules in a Rails app?
- Put model mixins in app/models/concerns and controller mixins in app/controllers/concerns. Both are autoload roots, so app/models/concerns/taggable.rb must define Taggable, not Concerns::Taggable. Namespaced helpers go in lib, which Rails autoloads via config.autoload_lib (added in Rails 7.1, generated by default since), so lib/billing/tax.rb must define Billing::Tax.
Published , Updated
Wondering what you can do next?
- Subscribe to our Ruby Magic newsletter and never miss an article again.
- Start monitoring your Ruby app with AppSignal.
- Share this article on social media

Cleiviane Costa
Our guest author Cleiviane Costa is a software engineer with 10+ years of experience. She's passionate about Ruby on Rails and loves to write about things that she discovers in her day-to-day activities.
All articles by Cleiviane CostaBecome our next author!
AppSignal monitors your apps
AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!


