Skip to content

Ruby const_missing — Dynamic Constant Resolution and Autoloading Patterns Explained

DodaTech Updated 2026-06-28 6 min read

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