Code Loading: require and autoload

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18); features newer than Ruby 3.0 name the version that introduced them. A Ruby program is rarely one file. The language has no import statement and no module system tied to the file system: files are plain scripts, and loading a file means running it, top to bottom, in the Ruby process. Everything the file defines (classes, modules, constants, methods, global variables) becomes part of the running program. This page covers the machinery behind that: Kernel#require, Kernel#require_relative and Kernel#load, the load path ($LOAD_PATH) that decides where require looks, the loaded-features list ($LOADED_FEATURES) that makes require idempotent, circular requires, the difference between running a file as a script and loading it as a library, lazy loading with autoload (and the planned autoload_relative), the file and constant naming conventions that loaders such as the community gem Zeitwerk automate, and Ruby::Box, the experimental in-process isolation that arrived with Ruby 4.0.

Where the code comes from (installed gems, Gemfile entries, default and bundled gems) is the subject of RubyGems and Bundler; how a library is laid out, built and published as a gem is in Creating and Publishing Gems, and Rake; the -I, -r, -w and RUBYLIB options and RUBYOPT are collected in The Ruby Command Line and Environment. Constants, which is what autoload is keyed on, are explained in Variables, Constants and Scope; namespaces built from modules are in Modules and Mixins; the comparison between refinements and Ruby::Box is in Refinements and Monkey Patching; the exception classes that loading raises are introduced in Exceptions and Error Handling; and the structure of a whole application (where to put the files that these tools load) is in Application Architecture in Plain Ruby. For the same ideas in other languages see PHP namespaces and autoloading (which has its own autoloader standard).

Every example on this page was syntax-checked with ruby -wc, run with ruby -w on Ruby 4.0.7 and linted with RuboCop 1.92 using the configuration of the running shelf gem (target Ruby 4.0, double-quoted strings). The output shown in # => comments and in the output blocks is the real output; the version claims were additionally checked on Ruby 3.2, 3.3 and 3.4 where they could be. Each example lives in the examples/ directory of the shelf gem and is run from the gem’s root, with the library under lib/ as in the layout below. Machine-dependent paths are shortened in the output.

shelf/
├── examples/              the scripts of this page
├── exe/shelf              the command-line entry point
├── lib/
│   ├── shelf.rb           requires everything (require_relative)
│   └── shelf/
│       ├── version.rb     Shelf::VERSION
│       ├── isbn.rb        Shelf::Isbn
│       ├── author.rb      Shelf::Author
│       ├── book.rb        Shelf::Book
│       ├── loan.rb        Shelf::Loan
│       ├── catalog.rb     Shelf::Catalog
│       ├── store/
│       │   └── memory.rb  Shelf::Store::Memory
│       └── refinements/
│           └── title_case.rb  Shelf::Refinements::TitleCase
└── shelf.gemspec

The loading tools at a glance

Ruby has four ways to bring code into a running program, and a fifth that changes what "into the program" means.

Tool What it does Loads the file again? Where it looks

require "name"

Finds name.rb (or a native extension), runs it, records its absolute path in $LOADED_FEATURES.

No: returns false the next time.

The load path, unless the name is absolute or starts with ./ or ../.

require_relative "path"

Like require, with the path resolved against the directory of the file that calls it.

No.

Relative to the calling file.

load "file.rb"

Runs the file, every time. Needs the extension. Optional wrapping module.

Yes, on every call.

The load path, then the working directory.

autoload :Const, "path"

Registers a constant; the file is required the first time the constant is used.

No: it is a require once triggered.

The load path (it is a require), unless the path is absolute.

Ruby::Box#require

Runs the file in a separate box, so its constants, monkey patches and globals stay out of the main program (4.0, experimental).

Once per box.

The load path of that box.

The rule of thumb that follows from the table: use require for libraries found by name (the standard library and gems), require_relative for the files of your own project, load for code that must run again (a configuration file, a script reloaded in a console), and autoload (usually through a loader such as Zeitwerk) when starting up fast matters more than failing early.

require

Kernel#require loads a feature: a file named without its extension ("shelf/version"), with it ("shelf/version.rb"), or by path. Its contract, from the documentation, is short and worth knowing exactly.

  • It returns true when it loaded the file and false when the feature was already loaded. It never loads a file twice.

  • If the name is neither absolute nor starts with ./ or ../, the file is searched for in the directories of $LOAD_PATH ($:). A name starting with ./ or ../ is resolved against the current working directory (Dir.pwd), not against the requiring file and not against the load path. A bare relative name such as "lib/shelf" is searched only on the load path, so it does not find a lib/ directory in the working directory.

  • A .rb extension means a Ruby source file. .so, .o or the platform’s shared-library extension (.bundle on macOS, .dll on Windows) means a native extension. With no extension, Ruby tries .rb, then the native extensions, until one is found. If nothing is found it raises LoadError.

  • The absolute path of the loaded file is added to $LOADED_FEATURES ($"). The path is what is remembered, so require "a"; require "./a" loads a.rb once.

  • Constants, methods and global variables defined by the file are available to the whole program. Local variables are not: they stay in the file.

  • The argument may be anything that responds to to_path, such as a Pathname.

examples/require_basics.rb
# frozen_string_literal: true

# Make lib/ visible to require. Prepending keeps it ahead of installed gems.
lib = File.expand_path("../lib", __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)

# require returns true the first time and false when the feature is loaded.
Array.new(2) { require "shelf/version" } # => [true, false]
require File.join(lib, "shelf/version.rb") # => false, the same file by absolute path
puts Shelf::VERSION # prints 0.1.0

# The loaded file is recorded by absolute path in $LOADED_FEATURES ($").
$LOADED_FEATURES.grep(%r{shelf/version}).map { it.delete_prefix(lib) } # => ["/shelf/version.rb"]

# resolve_feature_path shows where require would look without loading anything.
kind, path = $LOAD_PATH.resolve_feature_path("shelf/isbn")
p kind # prints :rb
path.delete_prefix(lib) # => "/shelf/isbn.rb"
$LOAD_PATH.resolve_feature_path("shelf/missing") # => nil
$LOADED_FEATURES.grep(%r{shelf/isbn}) # => []

# A feature that cannot be found raises LoadError, which is a ScriptError and
# is therefore not rescued by a bare rescue.
begin
  require "shelf/missing"
rescue LoadError => e
  e.message # => "cannot load such file -- shelf/missing"
  e.path # => "shelf/missing"
end
[LoadError.superclass, ScriptError.superclass] # => [ScriptError, Exception]

# require also accepts anything with to_path, such as a Pathname.
require Pathname.new(lib).join("shelf/isbn") # => true
Shelf::Isbn.new("978-0-306-40615-7").to_s # => "9780306406157"

LoadError

A feature that cannot be found raises LoadError, whose message is cannot load such file — name and whose path is the name that was asked for. LoadError descends from ScriptError, not from StandardError, so a bare rescue => e does not catch it. Rescue it by name. The usual reasons are a name that is not on the load path (the most common: running ruby lib/foo.rb without -Ilib), a gem that is not installed or not listed in the Gemfile under Bundler, and a misspelled file name. A library that works without an optional dependency rescues the error deliberately and degrades (the example prints false because fast_isbn does not exist):

examples/optional_dependency.rb
# frozen_string_literal: true

# An optional accelerator: use it when installed, fall back when it is not.
begin
  require "fast_isbn"
  FAST = true
rescue LoadError => e
  raise unless e.path == "fast_isbn" # a missing dependency of fast_isbn is a real error

  FAST = false
end

puts FAST # prints false

LoadError#path is useful when a library wraps a require: it tells whether the missing file is the optional one or one of its dependencies, which should still fail loudly.

Native extensions and the extension rules

require "socket" loads the compiled socket.so (socket.bundle on macOS) from the architecture-specific directory on the load path; require "json" loads json.rb, which in turn requires its compiled parts. The extension is optional and conventionally omitted, which is what lets a library swap a pure Ruby implementation for a compiled one without changing its callers. Interoperating with C and Rust extensions is outside this page; the call that loads them is the same require.

require has a cost, and it is paid once

The first require of a feature walks the load path, reads, parses and runs the file. Later calls only look the path up in $LOADED_FEATURES. A require inside a method is therefore legal and cheap after the first call, and is a common way to postpone the cost of a rarely used dependency (def to_csv = (require "csv"; ...)); the price is that a missing library is found late, in production, instead of at boot. Top-of-file require lines are the default for that reason.

The load path

The load path is the array $LOAD_PATH (also $: and $-I) of directories that require and load search, in order. The first directory containing the feature wins. The variable itself is read-only: assigning to it raises NameError ($LOAD_PATH is a read-only variable), but the array is an ordinary array that can be changed in place.

Where the entries come from

Source Effect

Ruby’s own directories

site_ruby, vendor_ruby and the standard library directory, plus their architecture-specific counterparts. About eight entries on a default install.

-I dir on the command line

Prepended, ahead of Ruby’s own directories, and expanded to an absolute path when Ruby starts.

RUBYLIB environment variable

Directories separated by : (; on Windows), added at the start. Not expanded, so a relative entry stays relative.

RUBYOPT environment variable

Can carry -I and -r options for every ruby run in that shell.

$LOAD_PATH.unshift and $LOAD_PATH.push

unshift puts a directory first (it wins); push or << puts it last, after the standard library.

RubyGems (activation)

When require fails to find a feature on the load path, RubyGems searches the installed gems, activates the gem that provides it and inserts that gem’s lib directory into $LOAD_PATH.

Bundler (bundler/setup, bundle exec)

Replaces the dynamic search: only the gems of the Gemfile.lock are activated, all at once, before the program starts.

$ ruby -Ilib -e 'puts $LOAD_PATH.first; require "shelf"; puts Shelf::VERSION'
/path/to/shelf/lib
0.1.0

$ RUBYLIB=lib ruby -e 'puts $LOAD_PATH.first; require "shelf"; puts Shelf::VERSION'
lib
0.1.0

$ ruby -Ilib -rshelf -e 'puts Shelf::VERSION'
0.1.0

$ RUBYOPT="-Ilib -rshelf" ruby -e 'puts Shelf::VERSION'
0.1.0

$ ruby -rshelf -e '1'
<internal:.../rubygems/core_ext/kernel_require.rb>:139:in 'Kernel#require': cannot load such file -- shelf (LoadError)

The first two commands show the difference between -I (expanded to an absolute path) and RUBYLIB (kept as typed). The last shows the failure that gives rise to most LoadError questions: -r shelf is a require, and lib/ is not on the load path unless something put it there.

Order decides which file wins, and features are keyed by path

When two directories contain the same relative name, require takes the first. The loaded feature is recorded by the resolved absolute path, not by the name given to require. That has a consequence worth seeing: if the directory that supplied the first file leaves the load path, the same name resolves to a different file, which has not been loaded, so require loads it too and returns true.

examples/load_path_order.rb
# frozen_string_literal: true

require "tmpdir"

# Records which copy of greeting.rb ran.
module Shelf
  def self.sources = @sources ||= []
end

# Two directories both provide "greeting.rb"; the first one on $LOAD_PATH wins.
Dir.mktmpdir do |tmp|
  root = File.realpath(tmp)
  %w[vendored local].each do |name|
    Dir.mkdir(File.join(root, name))
    File.write(File.join(root, name, "greeting.rb"), "Shelf.sources << #{name.inspect}\n")
  end
  vendored = File.join(root, "vendored")
  local = File.join(root, "local")

  # push (like RUBYLIB) appends after the standard directories; unshift (like
  # the -I option) goes first.
  $LOAD_PATH.push(vendored)
  $LOAD_PATH.unshift(local)
  $LOAD_PATH.index(local) # => 0
  $LOAD_PATH.last.end_with?("/vendored") # => true

  $LOAD_PATH.resolve_feature_path("greeting").last.delete_prefix(root) # => "/local/greeting.rb"
  load_greeting = -> { require "greeting" }
  load_greeting.call # => true
  Shelf.sources # => ["local"]

  # $-I (like $:) is an alias of $LOAD_PATH.
  $-I.equal?($LOAD_PATH) # => true

  # Features are keyed by resolved path, not by the name given to require.
  # Once local/ is gone from the path, "greeting" resolves to the other file,
  # which has not been loaded, so require loads it too.
  $LOAD_PATH.delete(local)
  $LOAD_PATH.resolve_feature_path("greeting").last.delete_prefix(root) # => "/vendored/greeting.rb"
  load_greeting.call # => true
  Shelf.sources # => ["local", "vendored"]
  $LOADED_FEATURES.grep(%r{/greeting\.rb\z}).map { it.delete_prefix(root) }
  # => ["/local/greeting.rb", "/vendored/greeting.rb"]
end

$LOAD_PATH.resolve_feature_path(name) answers "what would require load?" without loading it: it returns [:rb, path] or [:so, path], or nil when the feature is not found (since Ruby 3.1 it returns nil instead of raising when the feature cannot be found). It is the best debugging tool for "why did I get the wrong file".

Pitfalls of the load path

  • Relative entries are resolved when require runs, against the working directory at that moment. $LOAD_PATH.unshift("lib") works until the program calls Dir.chdir, after which require fails with LoadError. Build entries from __dir__: File.expand_path("../lib", __dir__).

  • Prepend or append is a decision. unshift lets a vendored or local copy shadow an installed gem; push lets the installed one win. A test suite usually wants unshift (or -Ilib), so it tests the code in the working tree rather than a released copy.

  • Do not edit $LOAD_PATH inside a gem. A gem’s lib is added by RubyGems or Bundler from the require_paths of its gemspec; code that adds more entries makes load order depend on who ran first.

  • Do not shadow the standard library by accident. A project file named set.rb, json.rb or logger.rb in a directory that is ahead of the standard library on the load path replaces the real library for every require of that name, including the ones inside other libraries.

  • Subdirectory names are namespaces by convention. Put shelf.rb and shelf/ under one directory on the load path, and require "shelf/isbn"; do not add lib/shelf itself to the path, or require "isbn" becomes possible and collides with other libraries.

How RubyGems fits in

Kernel#require as you call it is not the C function in the interpreter: RubyGems replaces it with a wrapper that first tries the original (gem_original_require) and, if the feature is not on the load path, activates the gem that provides it and retries. That is why require "rake" works in a script that never mentions gems. The example shows the wrapper, the activation and the resulting load path entry. (The version in the output is the one installed on the test machine.)

examples/gem_activation.rb
# frozen_string_literal: true

# RubyGems wraps Kernel#require. When the name is not found on the load path
# it looks the feature up in the installed gems and activates the gem, which
# adds the gem's lib/ directory to $LOAD_PATH.
Kernel.private_method_defined?(:gem_original_require) # => true
File.basename(method(:require).source_location.first, ".rb>") # => "kernel_require"

$LOAD_PATH.grep(/rake/) # => []
Gem.loaded_specs.key?("rake") # => false

require "rake" # => true

Gem.loaded_specs.fetch("rake").name # => "rake"
$LOAD_PATH.grep(/rake/).map { it.split("/").last(3).join("/") } # => ["gems/rake-13.3.1/lib"]
$LOAD_PATH.index { it.include?("/rake-") } # => 0

Run with --disable-gems, require "rake" fails with LoadError: nothing but the load path is consulted. Under Bundler the same require finds only gems listed in the Gemfile.lock. This is also why a bundled gem that a project uses but did not list fails under Bundler even though it is installed with Ruby. In Ruby 4.0, logger, ostruct, pstore, benchmark, irb, rdoc, reline and fiddle (among others) are bundled gems rather than default gems, and with a Gemfile that does not list logger:

$ bundle exec ruby -e 'require "logger"'
-e:1: warning: logger used to be loaded from the standard library, but is not part of the default gems since Ruby 4.0.0.
You can add logger to your Gemfile or gemspec to fix this error.
<ruby>/bundled_gems.rb:60:in 'Kernel.require': cannot load such file -- logger (LoadError)

Source: Ruby 4.0.0 NEWS

The warning is helpful: it names the gem and the fix (a Gemfile or gemspec entry). The same check exists since Ruby 3.3 for the gems that were about to become bundled, and ran as a warning before it ran as an error. Libraries that depend on a bundled gem must declare it in the gemspec. The full picture of default versus bundled gems is in RubyGems and Bundler.

require_relative and the current directory

Kernel#require_relative loads a file named relative to the directory of the file that contains the call. It returns true or false like require, raises LoadError if the file does not exist, and records the same absolute path in $LOADED_FEATURES, so a file loaded through require_relative and then through require (when the load path reaches it) is loaded once.

It is the right tool for the files of your own project: it needs no load path setup, works from any working directory and does not depend on -I. The example is started from a different directory than its own (cd / and an absolute path to the script) and still finds its files.

examples/require_relative.rb
# frozen_string_literal: true

# require_relative resolves against this file's directory, not the working
# directory, so the script works from anywhere: ruby /path/to/require_relative.rb
require_relative "../lib/shelf/version"
require_relative "../lib/shelf/isbn"

puts Shelf::VERSION # prints 0.1.0
puts File.basename(__dir__) # prints examples
puts File.basename(__FILE__) # prints require_relative.rb
puts Dir.pwd == __dir__ # prints false when started from another directory

# The same file reached through the load path is the same feature: false.
lib = File.expand_path("../lib", __dir__)
$LOAD_PATH.unshift(lib)
require "shelf/isbn" # => false

# Features are stored as absolute paths, so the file is recorded only once.
$LOADED_FEATURES.count { it.end_with?("/shelf/isbn.rb") } # => 1
$ cd / && ruby -w /path/to/shelf/examples/require_relative.rb
0.1.0
examples
require_relative.rb
false
  • __FILE__ is the path of the current file as Ruby was given it (relative if the script was started with a relative path).

  • __dir__ is the absolute directory of the current file, with symbolic links resolved (the directory of File.realpath(__FILE__)). Prefer it to File.dirname(__FILE__), which is not absolute. In ruby -e and in a script read from standard input it is "."; inside an eval without a file name it is nil.

  • $0 and $PROGRAM_NAME are the name of the script Ruby was started with.

  • require_relative has no file to be relative to when the code does not come from a file. In ruby -e '...' and in a script piped to standard input it resolves against the working directory, and in an eval of a string with no file name it raises LoadError with the message cannot infer basepath.

  • require_relative is not a drop-in replacement for require in a gem’s public entry point when other tools (the Zeitwerk loader’s for_gem, for one) expect the file to be loaded by its feature name.

$ ruby -e 'begin; require_relative "x"; rescue LoadError => e; puts e.message; end'
cannot load such file -- /path/to/working/directory/x

$LOADED_FEATURES

$LOADED_FEATURES (also $") is the array of absolute paths of every file require has loaded, native extensions included. It starts with what the interpreter itself loaded (enumerator.so, thread.rb, fiber.so, rational.so, …​ about fifty entries on 4.0), then grows as the program runs. require consults it before touching the file system, which makes the whole mechanism idempotent.

  • Inspecting it is the way to see which libraries a program really loaded and from where ($LOADED_FEATURES.grep(/json/)). The -r options and RUBYOPT entries are in it before the first line of the script.

  • Removing an entry makes require load the file again, with the side effects that implies: constants are redefined (a warning with -w) and at_exit hooks or registrations run twice. Editing the list is a hack for reloading code in a console, not a design; the supported tools are load and a reloading loader such as Zeitwerk.

  • Native extensions are in the list under their .so or .bundle name, which is how require "socket" is satisfied without scanning the file system a second time.

  • Concurrency. require is safe to call from several threads: a feature that one thread is loading is waited for by the others and is not loaded twice. From Ruby 3.4 require in a non-main Ractor is allowed and the loading itself runs on the main Ractor (Ractor._require exists to ask for it explicitly).

Circular requires

If store.rb requires catalog.rb and catalog.rb requires store.rb, the inner require finds store.rb already in progress and returns false immediately instead of recursing forever. The program then continues with a half-defined store.rb: any constant that store.rb defines after its require line is missing at that point. With warnings enabled (ruby -w, or $VERBOSE = true) Ruby reports the cycle.

examples/circular_require.rb
# frozen_string_literal: true

require "stringio"
require "tmpdir"

# store.rb needs catalog.rb, and catalog.rb needs store.rb: a cycle.
Dir.mktmpdir do |tmp|
  dir = File.realpath(tmp)
  File.write(File.join(dir, "store.rb"), <<~RUBY)
    require "catalog"
    module Cyclic; STORE = :store; end
  RUBY
  File.write(File.join(dir, "catalog.rb"), <<~RUBY)
    require "store"
    module Cyclic; CATALOG = :catalog; end
  RUBY
  $LOAD_PATH.unshift(dir)

  # With warnings on ($VERBOSE true, as with ruby -w) Ruby reports the cycle.
  # Warnings go to $stderr, which is redirected here to show the text.
  original = $stderr
  $stderr = StringIO.new
  begin
    require "store" # => true
  ensure
    warning = $stderr.string
    $stderr = original
  end
  puts warning[/loading in progress.*$/].gsub(dir, "<dir>")
  # prints loading in progress, circular require considered harmful - <dir>/store.rb

  # Nothing failed: the inner require returned false for the file still loading.
  Cyclic.constants.sort # => [:CATALOG, :STORE]
end

The warning is also followed by a backtrace in the real output; the example prints only the first line. Without -w there is no warning at all, which is why cycles survive in code bases that never run with warnings. They are usually harmless until a file starts to use at load time (as a superclass, in a include, in a constant’s value) something the cycle has not yet defined. The cures are the usual ones: move the shared definition into a third file that both require, require at the point of use (inside the method), or let autoload or a loader resolve constants lazily so that no file needs another at load time.

load

Kernel#load(filename, wrap = false) runs the Ruby program in a file, every time it is called. It does not consult or update $LOADED_FEATURES, and it returns true.

  • The name needs the extension (load "plugin" raises LoadError; load "plugin.rb" works).

  • An absolute path is used as it is; a path starting with ./ or ../ is resolved against the working directory; any other name is searched on $LOAD_PATH and, if it is not found there, taken relative to the working directory.

  • With wrap set to true, the file runs inside an anonymous module: its top-level constants and methods go into that module and the rest of the program does not see them. With a module as the second argument (Ruby 3.1), the file runs under that module, so the constants it defines are constants of the module.

  • As with require, local variables never propagate to the caller.

examples/load_vs_require.rb
# frozen_string_literal: true

require "tmpdir"

# A record of every time a plugin file runs.
module Shelf
  def self.runs = @runs ||= []
end

Dir.mktmpdir do |dir|
  plugin = File.join(dir, "plugin.rb")
  File.write(plugin, <<~RUBY)
    Shelf.runs << (plugin_local = :plugin)
    class PluginHelper; end
  RUBY

  # require runs a file once; load runs it every time it is called.
  Array.new(2) { require(plugin) } # => [true, false]
  Shelf.runs.size # => 1
  Array.new(2) { load(plugin) } # => [true, true]
  Shelf.runs.size # => 3

  # Locals stay in the file; constants and methods land in the caller's namespace.
  binding.local_variable_defined?(:plugin_local) # => false
  Object.const_defined?(:PluginHelper) # => true

  # load needs the full file name, extension included.
  begin
    load(File.join(dir, "plugin"))
  rescue LoadError => e
    e.message.sub(dir, "<dir>") # => "cannot load such file -- <dir>/plugin"
  end

  # load(file, true) runs the file under an anonymous module; load(file, mod)
  # (Ruby 3.1) runs it under mod. Nothing leaks into Object.
  wrapped = File.join(dir, "wrapped.rb")
  File.write(wrapped, "class WrappedHelper; end\n")
  load(wrapped, true) # => true
  Object.const_defined?(:WrappedHelper) # => false

  sandbox = Module.new
  load(wrapped, sandbox) # => true
  sandbox.const_defined?(:WrappedHelper, false) # => true
  Object.const_defined?(:WrappedHelper) # => false
end

load is for code whose purpose is to run again: a user configuration file read at start, a script reloaded in a console, a test fixture. Two details bite in practice. First, a loaded file that defines a constant redefines it every time, and Ruby warns (already initialized constant) under -w; keep such files to assignments that do not repeat definitions, or use wrap. Second, load does not look at the feature list, so a file loaded with load and later with require runs twice.

Running a file as a script or as a library

The same file is often both: an executable and something other code requires (for its tests, or to reuse a function). The idiom compares __FILE__, the path of the current file, with $PROGRAM_NAME, the path of the file Ruby was started with. They are equal only for the file that was started directly.

examples/main_guard.rb
# frozen_string_literal: true

require_relative "../lib/shelf"

module Shelf
  # Importable helper, safe to require from tests and other scripts.
  def self.summary(catalog = Catalog.new) = "#{catalog.count} books"
end

# Runs only when the file is the program, not when it is required.
puts Shelf.summary if __FILE__ == $PROGRAM_NAME
$ ruby -w examples/main_guard.rb
0 books

$ ruby -w -e 'require "./examples/main_guard"; puts "required, then: #{Shelf.summary}"'
required, then: 0 books

The first command prints the summary because the file is the program; the second requires it and prints only the caller’s line, because inside the required file __FILE__ is the file and $PROGRAM_NAME is -e. The shelf executable (exe/shelf) takes the other route and is only ever a program: it starts with require "shelf", which works because RubyGems puts the installed gem’s lib on the load path (and bundle exec, -Ilib or a binstub do the same during development). The __FILE__ == $PROGRAM_NAME guard is not needed in an executable, only in a file with two lives.

autoload

autoload registers a constant without defining it: the first time the constant is referenced, Ruby requires the file and expects the file to define the constant. Until then nothing is read from the disk. Two forms exist.

  • Module#autoload(const, filename) registers const (a symbol or a string) in that module, Shelf.autoload(:Isbn, "shelf/isbn"). Inside a module or class body the receiver is implicit.

  • Kernel#autoload(const, filename) registers the constant in the current lexical namespace (the module or class being defined, or Object at the top level).

The details, all from the documentation:

  • The file is loaded with Kernel#require, so the usual search rules apply: a relative name is searched on $LOAD_PATH, and an absolute path skips the search.

  • If the constant is already registered as an autoload, the new file name replaces the old; if it is already defined, the call does nothing. Files that are currently being loaded must not be registered for autoload.

  • autoload?(name, inherit = true) returns the registered file name (or nil if there is none or the constant is already loaded), searching the ancestors unless inherit is false. It does not load anything.

  • Referencing the constant is what triggers the load. defined?(Shelf::Isbn) and const_defined? answer without loading; Module#constants lists autoload constants; Module#const_source_location returns the file and line of the autoload call until the constant has been loaded, and the file and line of the definition afterwards.

examples/autoload_shelf.rb
# frozen_string_literal: true

lib = File.expand_path("../lib", __dir__)
$LOAD_PATH.unshift(lib)

require "shelf/version"

# Register constants instead of requiring their files; nothing is read yet.
module Shelf
  autoload :Isbn, "shelf/isbn"
  autoload :Author, "shelf/author"
  autoload :Book, "shelf/book"
  autoload :Catalog, "shelf/catalog"

  module Store
    autoload :Memory, "shelf/store/memory"
  end
end

loaded = -> { $LOADED_FEATURES.grep(%r{/lib/shelf/}).map { File.basename(it, ".rb") } }
loaded.call # => ["version"]

# The registration is visible, and querying it does not load the file.
Shelf.autoload?(:Isbn) # => "shelf/isbn"
Shelf.const_defined?(:Isbn) # => true
puts defined?(Shelf::Isbn) # prints constant
Shelf.constants.sort.first(3) # => [:Author, :Book, :Catalog]
loaded.call # => ["version"]
puts Shelf.const_source_location(:Isbn).last # prints 10, the line of the autoload call

# The first real use of the constant requires the file, then continues.
isbn = Shelf::Isbn.new("978-0-306-40615-7")
isbn.to_s # => "9780306406157"
loaded.call # => ["version", "isbn"]
Shelf.autoload?(:Isbn) # => nil
Shelf.const_source_location(:Isbn).first.delete_prefix(lib) # => "/shelf/isbn.rb"

# Constants that are never used are never loaded.
Shelf::Store::Memory.new.all # => []
loaded.call # => ["version", "isbn", "memory"]
Shelf.autoload?(:Loan) # => nil, no autoload was registered for it

What goes wrong

The contract is that the file defines the constant. The ways to break it have different exceptions, and one of them is a trap:

examples/autoload_pitfalls.rb
# frozen_string_literal: true

require "stringio"
require "tmpdir"

module Shelf
  Report = Class.new
end

# Autoloads that point at a file with the wrong constant, a crashing file and
# a file that does not exist.
def register_autoloads(dir)
  File.write(File.join(dir, "reports.rb"), "module Shelf; module Summary; end; end\n")
  File.write(File.join(dir, "broken.rb"), "raise ArgumentError, 'broken on purpose'\n")
  $LOAD_PATH.unshift(dir)

  Shelf.autoload(:Reports, "reports")
  Shelf.autoload(:Broken, "broken")
  Shelf.autoload(:Missing, "no_such_file")
end

# Run the block with warnings captured, as ruby -w would print them.
def capture_warnings
  original = $stderr
  $stderr = StringIO.new
  yield
  $stderr.string
ensure
  $stderr = original
end

Dir.mktmpdir do |tmp|
  register_autoloads(File.realpath(tmp))

  # 1. The file loaded but did not define the constant: NameError, not LoadError.
  warning = capture_warnings do
    Shelf::Reports
  rescue NameError => e
    e.message # => "uninitialized constant Shelf::Reports"
  end
  warning.sub(/\A.*?: /, "") # => "warning: Expected reports to define Shelf::Reports but it didn't\n"
  Shelf.autoload?(:Reports) # => nil, the registration is gone after the attempt

  # 2. The file does not exist: LoadError, and the registration stays.
  begin
    Shelf::Missing
  rescue LoadError => e
    e.message # => "cannot load such file -- no_such_file"
  end
  Shelf.autoload?(:Missing) # => "no_such_file"

  # 3. The file raises: the exception propagates and the next access retries.
  2.times do
    Shelf::Broken
  rescue ArgumentError => e
    e.message # => "broken on purpose"
  end

  # 4. A constant that is already defined is left alone: the autoload is ignored.
  Shelf.autoload(:Report, "never_loaded") # => nil
  Shelf.autoload?(:Report) # => nil
end

The cases in the output are worth stating plainly:

Situation What Ruby does What to do

The file loads but does not define the constant

NameError (uninitialized constant), plus a warning under -w (Expected reports to define Shelf::Reports but it didn't). The registration is dropped, so a second reference is a plain NameError.

Name the file after the constant it defines. This is the rule that Zeitwerk checks for you.

The file does not exist

LoadError; the registration stays.

Fix the path; keep it absolute, or relative to a directory on the load path.

The file raises while loading

The exception propagates to the code that referenced the constant; the next reference tries again.

Fix the file. A half-loaded file may have defined part of its constants already.

The constant is already defined

autoload does nothing.

Register the autoload before anything defines the constant; the usual place is the namespace’s own file.

Two threads reference the constant at once

One thread loads; the others wait and then see the constant. The file runs once (checked on Ruby 3.2, 3.4 and 4.0 with three threads and a file that sleeps while loading). Several races were fixed in Ruby 3.2.

Nothing to do; this is what makes autoload safe for libraries.

What autoload cannot do

  • It is keyed on a constant. Code that has to run at load time for its side effects (registering a plugin, defining a refinement, patching a core class) cannot be autoloaded, since nothing references it. Shelf::Refinements::TitleCase can be autoloaded because callers write its name in using, but a file that only monkey patches String must be required.

  • It is lazy, and lazy means that errors surface late: a typo in a rarely used file is found when the file is first used. Test suites for projects that rely on autoload (including Zeitwerk) therefore eager load once in CI.

  • It changes when code runs, which can surprise: a class-body inherited hook, a Struct or Data definition and a Module#included callback all fire at first use rather than at boot.

  • Subclassing across files needs the superclass at load time, which is fine (the reference triggers its load), but a cycle of superclass or include references between autoloaded files fails in the same way as a cycle of `require`s.

autoload_relative (Ruby 4.1, preview)

The Ruby 4.1 preview NEWS lists Kernel#autoload_relative and Module#autoload_relative: they work like autoload but resolve the file path relative to the file where the method is called, the way require_relative does, so that a namespace file can register its siblings without a hard-coded absolute path or a load path change. Ruby 4.1 had not been released when this page was written and the methods do not exist in Ruby 4.0 (Module.method_defined?(:autoload_relative) is false on 4.0.7 and on 3.2 to 3.4); the description comes from the NEWS of the development branch and was not run. Until it ships, write the path with File.expand_path("shelf/isbn", __dir__) or use a loader.

const_missing

Module#const_missing is the older, more general hook: Ruby calls it when a constant is not found, and the default raises NameError. A class can define its own to load a file by the constant’s name (the documentation shows one that downcases the constant name and requires that file). It differs from autoload in important ways: it runs only after the lookup has failed everywhere, it is not visible to constants, autoload? or defined?, and it does not know the lexical scope of the reference, which is the reason Rails moved off it when it adopted Zeitwerk. New code should use autoload or a loader.

File and constant naming conventions

A loader can only guess which file defines which constant if the project follows a rule. The rule that the Ruby community settled on, and that Zeitwerk makes mandatory, is that the path of a file is the path of the constant it defines:

File Constant Rule

lib/shelf.rb

Shelf

Top-level file named after the namespace; the directory lib/shelf/ holds the namespace’s contents.

lib/shelf/isbn.rb

Shelf::Isbn

Directory = enclosing module; file = one constant.

lib/shelf/store/memory.rb

Shelf::Store::Memory

Nested namespaces are nested directories.

lib/shelf/refinements/title_case.rb

Shelf::Refinements::TitleCase

The file name is the constant name in snake case (TitleCase becomes title_case).

lib/shelf/store/, with no store.rb

Shelf::Store

A directory with Ruby files and no file of its own is an implicit namespace: Zeitwerk creates the module for you.

lib/shelf/version.rb

Shelf::VERSION (not Shelf::Version)

The exception: an all-caps constant. Zeitwerk’s gem inflector expects exactly this file to define VERSION.

The conventions in short: one top-level class or module per file, the file name is the snake-case form of the constant name, a directory is a namespace, and a namespace that has behaviour of its own gets a file next to its directory (catalog.rb and catalog/). Acronyms are the weak point: HTMLParser and html_parser.rb do not map onto each other by a simple rule, so every loader has an escape hatch (Zeitwerk’s inflector.inflect("html_parser" => "HTMLParser")), and many code bases avoid acronyms in class names. Defining several constants in one file, or a constant that does not match its file name, is what the conventions rule out. Constants that are not classes or modules (a VERSION string, a DEFAULTS hash) are defined inside a class or module, not in a file of their own, except for the special cases above.

The same inference runs in the other direction in application code: given a class name, derive the file name. The mapping is a snake-case conversion of each name segment joined with /, and Object.const_get("Shelf::Store::Memory") resolves a constant path given as a string, which triggers any autoload on the way. The example is a miniature loader built from just these pieces: it walks lib/shelf, creates a module per directory, registers one autoload per file, and shows where the convention breaks (version.rb, acronyms).

examples/mini_loader.rb
# frozen_string_literal: true

# A miniature of what Zeitwerk automates: derive constant names from file
# names and register one autoload per file, one module per directory.
module MiniLoader
  # "title_case" -> "TitleCase", "isbn" -> "Isbn"
  def self.camelize(basename) = basename.split("_").map(&:capitalize).join

  # "Shelf::Store::Memory" -> "shelf/store/memory"
  def self.path_for(name)
    name.split("::").map { it.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase }.join("/")
  end

  def self.setup(namespace, dir)
    Dir.children(dir).sort.each do |entry|
      path = File.join(dir, entry)
      if File.directory?(path)
        setup(namespace.const_set(camelize(entry), Module.new), path)
      elsif entry.end_with?(".rb")
        namespace.autoload(camelize(entry.delete_suffix(".rb")), path)
      end
    end
  end
end

lib = File.expand_path("../lib", __dir__)

Shelf = Module.new

MiniLoader.setup(Shelf, File.join(lib, "shelf"))

# version.rb defines VERSION, not Version, so the convention breaks for it;
# Zeitwerk's GemInflector special-cases exactly this file.
Shelf.autoload?(:Version).delete_prefix(lib) # => "/shelf/version.rb"
Shelf.constants.sort # => [:Author, :Book, :Catalog, :Isbn, :Loan, :Refinements, :Store, :Version]

Shelf::Store::Memory.new.all # => []
Shelf::Refinements::TitleCase.refinements.size # => 1
Shelf.autoload?(:Isbn).delete_prefix(lib) # => "/shelf/isbn.rb"

# Constant path <-> file path, the inference that Zeitwerk's checks rely on.
MiniLoader.path_for("Shelf::Store::Memory") # => "shelf/store/memory"
MiniLoader.camelize("title_case") # => "TitleCase"
Object.const_get("Shelf::Store::Memory").instance_method(:add).owner # => Shelf::Store::Memory

# Acronyms break the round trip: the file html_parser.rb maps to HtmlParser, so a
# class named HTMLParser needs an explicit inflection rule.
MiniLoader.path_for("HTMLParser") # => "htmlparser"
MiniLoader.camelize("html_parser") # => "HtmlParser"

Zeitwerk (community gem)

Zeitwerk is the code loader used by Rails since Rails 6 and by many gems. It is a community project, not part of Ruby: the facts below come from its README and were not run here (the gem is not installed on the test machine). It does what the miniature above does, plus the parts that make it production-ready: it is thread-safe, descends into subdirectories lazily, and uses absolute paths for its require calls so that no load path search is involved.

examples/zeitwerk_entry.rb
# frozen_string_literal: true

# lib/shelf.rb as it would look with Zeitwerk (a community gem, not part of
# Ruby). This file is checked for syntax only: the gem is not installed here.
require "zeitwerk"

loader = Zeitwerk::Loader.for_gem
loader.setup # from here on, Shelf::Isbn, Shelf::Store::Memory, ... load on demand

module Shelf
  class Error < StandardError; end
end

loader.eager_load if ENV["SHELF_EAGER_LOAD"] # optional: load everything up front

Source: Zeitwerk README

  • Zeitwerk::Loader.for_gem is the shortcut for a gem whose entry point is lib/my_gem.rb: it manages lib as the root directory and expects lib/my_gem/version.rb to define MyGem::VERSION. The entry point must be loaded by Kernel#require (the way a gem is normally loaded), not load or require_relative.

  • The generic form is Zeitwerk::Loader.new with push_dir; setup finishes the configuration.

  • eager_load loads every managed file up front, and is the way a test suite checks that every file defines the constant its name promises: a violation raises Zeitwerk::NameError, a subclass of NameError.

  • enable_reloading (before setup) and reload support code reloading in development; reloading is opt-in and client code must coordinate it with running threads.

  • loader.inflector.inflect("html_parser" => "HTMLParser") maps a file to a non-standard constant name; loader.ignore skips files that do not follow the convention; loader.collapse treats a directory as organisational rather than a namespace.

  • Zeitwerk warns about circular dependencies that plain Ruby cannot resolve either: a class that needs, in its body, a constant that needs the first class cannot be loaded by any loader.

  • From version 2.7 it requires Ruby 3.2 or newer.

A Rails application does not call Zeitwerk itself: Rails configures one loader for app/ and one for each engine. Rails is outside the scope of this section, and the point here is only that the conventions above are the ones those applications follow.

Bootsnap (community gem)

Bootsnap is a different kind of tool, also community-maintained: it does not define where files go, but makes require cheaper by caching the results of load path searches and compiled bytecode on disk, which shortens boot time in large applications. It is configured after bundler/setup. It is useful to know that it exists because it replaces parts of require; the Ruby 3.3 warnings about soon-to-be-bundled gems are suppressed when Bootsnap is in use.

Source: Bootsnap README

Ruby::Box (Ruby 4.0, experimental)

Every file loaded into a Ruby process shares one set of classes, so one library’s monkey patch of String is every library’s problem (see Refinements and Monkey Patching for the language-level tool). Ruby::Box, added in Ruby 4.0, is a runtime-level answer: a box is a separate space inside the process, with its own classes, modules, constants, global variables, load path and loaded-features list. Code loaded in one box can reopen String without anyone in another box seeing it. It is the Ruby counterpart of a namespace-per-plugin, aimed at isolating applications, libraries and monkey patches from each other.

The feature is experimental in Ruby 4.0. The documentation says it may change, lists known issues, and Ruby prints a warning at start-up when it is on. Do not build production code on it; treat this section as a description of a technology that is likely to evolve (the documentation page is the authority).

Enabling it

Boxes are off by default. They are turned on for the whole process by the environment variable RUBY_BOX=1 (the only valid value), set before Ruby starts: setting it from inside the program has no effect. -W:no-experimental hides the start-up warning.

$ ruby -e 'p Ruby::Box.enabled?; p Ruby::Box.current; Ruby::Box.new'
false
nil
-e:1:in 'Ruby::Box#initialize': Ruby Box is disabled. Set RUBY_BOX=1 environment variable to use Ruby::Box. (RuntimeError)

$ RUBY_BOX=1 ruby -W:no-experimental -e 'p Ruby::Box.enabled?; p Ruby::Box.main; p Ruby::Box.root'
true
#<Ruby::Box:3,user,main>
#<Ruby::Box:2,root>

Using a box

Ruby::Box.new creates a box. Box#require, Box#require_relative and Box#load load a file in that box, and everything the file requires in turn is loaded there too. Constants defined in the box are reached through the box object (box::LoudTitles), and Box#eval evaluates a string in it. Box#load_path is the box’s own $LOAD_PATH.

examples/box_demo.rb
# frozen_string_literal: true

# Experimental in Ruby 4.0: run with RUBY_BOX=1 ruby examples/box_demo.rb
# (add -W:no-experimental to hide the experimental warning).
abort "start with RUBY_BOX=1" unless Ruby::Box.enabled?

require "tmpdir"

Dir.mktmpdir do |tmp|
  # A "plugin" that monkey patches String and defines its own constant.
  plugin = File.join(tmp, "loud_titles.rb")
  File.write(plugin, <<~RUBY)
    class String
      def shout = upcase + "!"
    end

    module LoudTitles
      def self.call(title) = title.shout
    end
  RUBY

  box = Ruby::Box.new
  box.require(plugin) # => true

  # The code in the box sees its own patched String ...
  puts box::LoudTitles.call("ruby basics") # prints RUBY BASICS!

  # ... but the main program is untouched.
  "ruby basics".respond_to?(:shout) # => false
  Object.const_defined?(:LoudTitles) # => false
  box.const_defined?(:LoudTitles) # => true

  # Each box keeps its own load path and its own list of loaded features.
  box.require(plugin) # => false
  box.eval("$LOADED_FEATURES").include?(plugin) # => true
  $LOADED_FEATURES.include?(plugin) # => false
  box.load_path.equal?($LOAD_PATH) # => false

  # The program itself runs in the main box; boxes you create are separate.
  Ruby::Box.current.equal?(Ruby::Box.main) # => true
  box.equal?(Ruby::Box.main) # => false
  box.eval("Ruby::Box.current").equal?(box) # => true
  box.eval("'ruby basics'.shout") # => "RUBY BASICS!"
end
$ RUBY_BOX=1 ruby examples/box_demo.rb
ruby: warning: Ruby::Box is experimental, and the behavior may change in the future!
See https://docs.ruby-lang.org/en/4.0/Ruby/Box.html for known issues, etc.
RUBY BASICS!

The rules, from the documentation

Topic Behaviour

Kinds of box

A root box where the built-in classes live, a main box in which the program (and the -r libraries) run, and any number of user boxes from Ruby::Box.new. There is also a master box that is only the template boxes are copied from. Ruby::Box.current, .main and .root return them; .enabled? reports whether RUBY_BOX=1 is in effect.

Scope

A box works at file scope: a .rb file runs in a single box, and every method and proc created in it runs in that box afterwards.

Built-in classes

Visible in every box and reopenable. A reopened String is changed only in the box that reopened it; other boxes keep the original. box::String is the same class as String, seen from the current box.

Classes defined in a box

Reached from outside as box::Name; inside the box the plain Name works.

Constants, class instance and class variables, globals

Separate per box, including the ones on built-in classes. A global (even $VERBOSE) set in a box is not seen outside it.

Top-level methods

Private instance methods of Object in that box; there is no way to expose them to other boxes.

Load path and loaded features

Each box has its own (checked above): a file required in one box is loaded again when another box requires it.

RubyGems and friends

RubyGems, ErrorHighlight, DidYouMean and SyntaxSuggest are loaded in each box independently.

The documentation also lists known issues (installing native extensions may fail under RUBY_BOX=1, require "active_support/core_ext" may fail, methods defined in a box may not be seen by built-in methods written in Ruby), and notes a performance cost for calls made from C when boxes are enabled. Neither refinements nor boxes replace the other: a refinement is a lexical, opt-in view of a patch inside one program, while a box is an isolated copy of the class space that one part of the program loads into. Before relying on either to sandbox untrusted code, remember that a box separates definitions, not capabilities: code in a box can still read files and open sockets.

Pitfalls and a decision guide

If you want to…​ Use Because

Load a file of your own project

require_relative

It does not depend on the working directory or the load path.

Load the standard library or a gem

require "name", with the gem in the Gemfile

Resolved through the load path or Bundler.

Make lib/ visible to a script or a test run

-Ilib, RUBYLIB=lib, or $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))

lib/ is not on the load path by default; absolute and prepended wins.

Run a file again each time

load

require runs once; load always runs.

Isolate the names a file defines

load(file, true) or load(file, mod)

The file’s constants go into a module, not Object.

Defer the cost of a large dependency

require inside the method, or autoload

Loaded on first use; failures show up late.

Load a whole library tree by naming convention

Zeitwerk (or a miniature of it)

One rule replaces dozens of require lines and is verified by eager_load.

See why a require loaded the wrong file

$LOAD_PATH.resolve_feature_path(name) and $LOADED_FEATURES.grep

Shows the resolution without side effects.

Keep one plugin’s monkey patches away from the rest

A refinement; as an experiment, Ruby::Box

Refinements are stable and lexical; boxes are process-wide and experimental.

  • require with a bare relative path does not search the working directory. require "lib/shelf" fails; require "./lib/shelf" works and is resolved against Dir.pwd, so it breaks when the program is started elsewhere.

  • A feature is identified by the real file. Two spellings of the same file, including a path through a symbolic link, are the same feature and are loaded once (checked on Ruby 3.2, 3.4 and 4.0).

  • rescue => e does not catch LoadError. It is a ScriptError. Rescue LoadError explicitly, and re-raise it if its path is not the one you expected.

  • Do not reassign $LOAD_PATH. It is read-only; change the array in place.

  • Top-level autoload registers on Object. Inside module Shelf ... end it registers on Shelf. Check autoload? on the namespace you meant.

  • Do not put a require of your own namespace into the file that defines it. The circular warning above is the symptom.

Where older material is outdated

Much written about Ruby code loading predates modern Ruby. When an old guide or code base disagrees with this page, the table says what changed.

Older practice Ruby 4.0

require "rubygems" at the top of every script

RubyGems is loaded at start-up and wraps require; the line is redundant.

require "set"

Set was made available without require (autoloaded) in Ruby 3.2 and is a core class in 4.0. The require still works, as a no-op.

require "pathname"

Pathname was promoted from a default gem to a core class in 4.0, so it is available without require (on 3.4 the require is needed).

require "ostruct", "logger", "benchmark", "pstore", "csv", "base64", "observer", …​ as "standard library"

They are bundled gems (4.0: ostruct, logger, benchmark, pstore, irb, rdoc, reline, fiddle; earlier: csv, base64, bigdecimal, observer, drb, …​). Under a Gemfile they must be listed or require fails.

File.dirname(__FILE__) and File.expand_path("../x", __FILE__)

__dir__ and require_relative do the same more directly.

$LOAD_PATH.unshift(File.expand_path("../lib", __FILE__)) before every require in a script

require_relative for the project’s own files; -Ilib or bundle exec for tests.

const_missing-based autoloading of application code

autoload and, above all, Zeitwerk, which supersedes the const_missing autoloader Rails used up to version 5.

Kernel#require called from several threads is "unsafe"

require and autoload are thread-safe; races in autoload were fixed in Ruby 3.2.

Load isolation by Module.new and load(file, true) only

Available since forever, still works; Ruby::Box (experimental) adds process-level isolation in 4.0.

Version notes

Version Change

3.1

Kernel#load accepts a module as the second argument and runs the file under it. $LOAD_PATH.resolve_feature_path returns nil instead of raising when the feature is not found.

3.2

Set is available without require (autoloaded). Several race conditions in Kernel#autoload fixed.

3.3

RubyGems and Bundler warn when a gem that is about to become a bundled gem is required without being listed (suppressed by Bootsnap).

3.4

require is allowed inside a Ractor and runs on the main Ractor (Ractor._require). csv, base64, bigdecimal and others become bundled gems.

4.0

Ruby::Box (experimental, RUBY_BOX=1). Set is a core class; SortedSet is gone. ostruct, logger, benchmark, pstore, irb, rdoc and others become bundled gems. Pathname is available without require. Fixes for require and autoload in a Ractor.

4.1 (preview)

Kernel#autoload_relative and Module#autoload_relative, announced in the development NEWS (not released).