Ruby const_missing — Dynamic Constant Resolution and Autoloading Patterns Explained
In this tutorial, you will learn about Ruby const_missing. We cover key concepts, practical examples, and best practices to help you master this topic.
Ruby const_missing intercepts references to undefined constants on a class or module, enabling autoloading patterns, dynamic constant generation, and Rails-style automatic class loading.
What You'll Learn
- Implementing const_missing for autoloading
- Dynamic constant generation
- Rails autoloading patterns
- Thread safety considerations
Why It Matters
const_missing powers Rails' autoloader, loading classes and modules on demand. Doda Browser uses const_missing for plugin discovery. Durga Antivirus Pro uses it for scanner module loading. It eliminates manual require statements.
Real-World Use
Rails' Zeitwerk autoloader, ActiveSupport's autoloading, dependency injection containers, plugin systems, and configuration registries.
flowchart LR
A["const_missing"] --> B["Autoloading"]
B --> C["Dynamic Constants"]
C --> D["Zeitwerk Pattern"]
D --> E["Thread Safety"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Basic const_missing
module Registry
def self.const_missing(name)
puts "[AUTO] Loading constant: #{name}"
const_set(name, Module.new)
end
end
Registry::MyClass # [AUTO] Loading constant: MyClass
Registry::Another # [AUTO] Loading constant: Another
puts Registry.constants.inspect # [:MyClass, :Another]
File-Based Autoloading
module Autoloader
@load_paths = ["."]
def self.const_missing(name)
@load_paths.each do |path|
file = File.join(path, name.to_s.downcase)
if File.exist?("#{file}.rb")
puts "[AUTOLOAD] Loading #{file}"
require file
if const_defined?(name)
return const_get(name)
end
end
end
raise NameError, "uninitialized constant #{name}"
end
end
# Simulated file structure:
# ./user.rb → User class
# ./product.rb → Product class
Rails-Style Autoloading
module Autoload
class << self
def registry
@registry ||= {}
end
def register(path)
Dir[File.join(path, "*.rb")].each do |file|
name = File.basename(file, ".rb").camelize
registry[name] = file
end
end
end
def self.const_missing(name)
file = registry[name.to_s]
if file && File.exist?(file)
require file
if const_defined?(name)
const_get(name)
else
raise NameError, "#{file} did not define #{name}"
end
else
raise NameError, "uninitialized constant #{name}"
end
end
end
# Usage: Autoload.register("app/models")
# Then: Autoload::User automatically loads app/models/user.rb
Dynamic Constant Generation
class Config
SETTINGS = {
db_host: "localhost",
db_port: 5432,
app_name: "MyApp",
log_level: "INFO"
}
def self.const_missing(name)
key = name.to_s.underscore
if SETTINGS.key?(key.to_sym)
const_set(name, SETTINGS[key.to_sym])
else
super
end
end
end
puts Config::DB_HOST # localhost
puts Config::DB_PORT # 5432
puts Config::APP_NAME # MyApp
puts Config.constants # [:DB_HOST, :DB_PORT, ...]
Factory Patternory" >}} Pattern with const_missing
class NotificationFactory
def self.for(type, *args)
klass_name = "#{type.to_s.camelize}Notification"
klass = const_get(klass_name)
klass.new(*args)
end
def self.const_missing(name)
if name.to_s.end_with?("Notification")
klass = Class.new(Notification) do
define_method(:deliver) { puts "Delivering #{self.class.name}" }
end
const_set(name, klass)
else
super
end
end
end
class Notification
def deliver
raise NotImplementedError
end
end
email = NotificationFactory.for(:email)
email.deliver # Delivering EmailNotification
sms = NotificationFactory.for(:sms)
sms.deliver # Delivering SMSNotification
push = NotificationFactory.for(:push)
push.deliver # Delivering PushNotification
const_missing with Thread Safety
require "monitor"
module ThreadSafeAutoloader
@mutex = Monitor.new
@loaded = {}
def self.const_missing(name)
@mutex.synchronize do
file = find_file(name)
if file
@loaded[name] = file
require file
if const_defined?(name)
return const_get(name)
end
end
super
end
end
def self.find_file(name)
path = name.to_s.gsub("::", "/").gsub(/([A-Z])/) { "_#{$1.downcase}" }.sub(/^_/, "")
file_paths = $LOAD_PATH.map { |lp| File.join(lp, "#{path}.rb") }
file_paths.find { |f| File.exist?(f) }
end
end
Module Registry Pattern
module PluginSystem
class << self
def register(name, mod = nil, &block)
if mod
const_set(name, mod)
else
mod = Module.new(&block)
const_set(name, mod)
end
end
def const_missing(name)
raise NameError, "Plugin #{name} is not registered. " \
"Available: #{constants.join(', ')}"
end
end
end
PluginSystem.register(:Logger) do
def self.log(msg)
puts "[LOG] #{msg}"
end
end
PluginSystem.register(:Alert, Module.new do
def self.alert(msg)
puts "[ALERT] #{msg}"
end
end)
PluginSystem::Logger.log("System started") # [LOG] System started
PluginSystem::Alert.alert("Security breach") # [ALERT] Security breach
Lazily Evaluated Constants
class LazyConstants
class << self
def lazy(name, &block)
lazy_loaders[name] = block
end
def lazy_loaders
@lazy_loaders ||= {}
end
def const_missing(name)
loader = lazy_loaders[name]
if loader
value = loader.call
const_set(name, value)
else
super
end
end
end
end
LazyConstants.lazy(:ExpensiveConfig) do
puts "Computing expensive config..."
{ host: "localhost", port: 8080 }
end
puts LazyConstants::ExpensiveConfig.inspect
# Computing expensive config...
# {:host=>"localhost", :port=>8080}
# Second access uses the cached constant
puts LazyConstants::ExpensiveConfig.inspect
# {:host=>"localhost", :port=>8080} (no computation)
Common Mistakes
1. Not Calling super for Unknown Constants
module Bad
def self.const_missing(name)
# Should call super for truly unknown names
"string" # Returns a string, not a constant!
end
end
# Bad::Unknown # Returns "string" instead of NameError
2. Thread Safety Without Mutex
module Unsafe
def self.const_missing(name)
# Race condition if two threads try to load the same constant
require name.to_s.downcase
const_get(name)
end
end
3. Infinite Recursion
module Loop
def self.const_missing(name)
const_set(name, "value")
const_get(name) # Fine — constant exists now
const_missing(name) # Calling self again — unnecessary
end
end
4. Confusing const_missing with method_missing
const_missing is a module/class-level hook. method_missing is an instance-level hook. They serve different purposes — one for constants, one for methods.
5. File Loading Without Tracking
If const_missing loads a file that defines a class, but the class name doesn't match the constant, subsequent const_missing will try to load the file again. Always check const_defined? after loading.
Practice Questions
1. When is const_missing called?
When Ruby encounters an undefined constant reference. SomeModule::UnknownConstant triggers const_missing(:UnknownConstant) on SomeModule.
2. How does Rails' autoloader use const_missing?
Rails maps constant names to file paths. When a constant is missing, it finds the matching file, loads it, and the constant becomes available. Zeitwerk now does this more efficiently.
3. What's the difference between const_missing and method_missing?
const_missing handles undefined constants (class/module references). method_missing handles undefined instance methods. They're separate hooks on different levels (class vs instance).
4. How do you make const_missing thread-safe?
Use a Mutex to ensure only one thread loads a given constant at a time. Check if the constant was loaded by another thread before proceeding.
Challenge: Implement a LazyRequire module that auto-loads classes from a specified directory when they're first referenced.
Solution
module LazyRequire
class << self
def load_path(path)
@paths ||= []
@paths << path
end
def const_missing(name)
@paths&.each do |path|
file = find_file(path, name)
next unless file
require file
if const_defined?(name)
return const_get(name)
end
end
super
end
private
def find_file(path, name)
# Convert CamelCase to snake_case
basename = name.to_s.gsub(/([A-Z])/) { "_#{$1}" }.downcase.sub(/^_/, "")
full_path = File.join(path, "#{basename}.rb")
File.exist?(full_path) ? full_path : nil
end
end
end
# Usage:
# LazyRequire.load_path("/app/models")
# LazyRequire::User → loads app/models/user.rb
FAQ
{{< faq question="Is const_missing still used in modern Rails?" >}}
Rails 6+ uses Zeitwerk, which uses Module#autoload instead of const_missing. It's more efficient and thread-safe. But const_missing is still useful for plugin systems and dynamic registries.
{{< /faq >}}
{{< faq question="Can I use const_missing with nested constants?" >}}
Yes. module A; end; A::B triggers A.const_missing(:B). For deeper nesting A::B::C, if B exists, B.const_missing(:C) is called.
{{< /faq >}}
{{< faq question="How do I debug const_missing issues?" >}}
Use Module#autoload? to check if a constant is pending. Use const_defined? to check after loading. Add logging in const_missing to trace which constants are being resolved.
{{< /faq >}}
{{< faq question="Can const_missing return a value that isn't a constant?" >}} Technically yes, but don't. const_missing should define and return a constant (Class, Module, or value). Returning arbitrary values confuses the constant resolution system. {{< /faq >}}
{{< faq question="How is const_missing different from autoload?" >}}
Module#autoload declares a constant that will be loaded on first reference — it's built into Ruby and more efficient. const_missing is a fallback hook. Prefer autoload when possible.
{{< /faq >}}
Try It Yourself
# const_missing_demo.rb
module Settings
@data = {
api_url: "https://api.example.com",
max_retries: 3,
timeout: 30
}
def self.const_missing(name)
key = name.to_s.downcase.to_sym
if @data.key?(key)
value = @data[key]
const_set(name, value)
else
raise NameError, "Unknown setting: #{name}"
end
end
end
puts Settings::API_URL # https://api.example.com
puts Settings::MAX_RETRIES # 3
puts Settings::TIMEOUT # 30
begin
Settings::UNKNOWN
rescue NameError => e
puts e.message # Unknown setting: UNKNOWN
end
What's Next
Now that you understand const_missing, explore eval and binding for runtime code evaluation.
| Topic | Description | Link |
|---|---|---|
| Ruby eval and binding | Runtime code evaluation | {{< ref "38-eval-binding" >}} |
| Ruby method_missing | Ghost methods, dynamic proxies | {{< ref "36-method-missing" >}} |
| Python import | Compare Python's dynamic imports | Python |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro