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
Source: RubyGems Guides: Patterns
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 |
|---|---|---|---|
|
Finds |
No: returns |
The load path, unless the name is absolute or starts with |
|
Like |
No. |
Relative to the calling file. |
|
Runs the file, every time. Needs the extension. Optional wrapping module. |
Yes, on every call. |
The load path, then the working directory. |
|
Registers a constant; the file is required the first time the constant is used. |
No: it is a |
The load path (it is a |
|
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
truewhen it loaded the file andfalsewhen 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 alib/directory in the working directory. -
A
.rbextension means a Ruby source file..so,.oor the platform’s shared-library extension (.bundleon macOS,.dllon Windows) means a native extension. With no extension, Ruby tries.rb, then the native extensions, until one is found. If nothing is found it raisesLoadError. -
The absolute path of the loaded file is added to
$LOADED_FEATURES($"). The path is what is remembered, sorequire "a"; require "./a"loadsa.rbonce. -
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 aPathname.
# 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):
# 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 |
|
|
Prepended, ahead of Ruby’s own directories, and expanded to an absolute path when Ruby starts. |
|
Directories separated by |
|
Can carry |
|
|
RubyGems (activation) |
When |
Bundler ( |
Replaces the dynamic search: only the gems of the |
$ 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.
# 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
requireruns, against the working directory at that moment.$LOAD_PATH.unshift("lib")works until the program callsDir.chdir, after whichrequirefails withLoadError. Build entries from__dir__:File.expand_path("../lib", __dir__). -
Prepend or append is a decision.
unshiftlets a vendored or local copy shadow an installed gem;pushlets the installed one win. A test suite usually wantsunshift(or-Ilib), so it tests the code in the working tree rather than a released copy. -
Do not edit
$LOAD_PATHinside a gem. A gem’slibis added by RubyGems or Bundler from therequire_pathsof 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.rborlogger.rbin a directory that is ahead of the standard library on the load path replaces the real library for everyrequireof that name, including the ones inside other libraries. -
Subdirectory names are namespaces by convention. Put
shelf.rbandshelf/under one directory on the load path, and require"shelf/isbn"; do not addlib/shelfitself to the path, orrequire "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.)
# 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.
# 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
Related building blocks
-
__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 ofFile.realpath(__FILE__)). Prefer it toFile.dirname(__FILE__), which is not absolute. Inruby -eand in a script read from standard input it is"."; inside anevalwithout a file name it isnil. -
$0and$PROGRAM_NAMEare the name of the script Ruby was started with. -
require_relativehas no file to be relative to when the code does not come from a file. Inruby -e '...'and in a script piped to standard input it resolves against the working directory, and in anevalof a string with no file name it raisesLoadErrorwith the messagecannot infer basepath. -
require_relativeis not a drop-in replacement forrequirein a gem’s public entry point when other tools (the Zeitwerk loader’sfor_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-roptions andRUBYOPTentries are in it before the first line of the script. -
Removing an entry makes
requireload the file again, with the side effects that implies: constants are redefined (a warning with-w) andat_exithooks or registrations run twice. Editing the list is a hack for reloading code in a console, not a design; the supported tools areloadand a reloading loader such as Zeitwerk. -
Native extensions are in the list under their
.soor.bundlename, which is howrequire "socket"is satisfied without scanning the file system a second time. -
Concurrency.
requireis 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.4requirein a non-main Ractor is allowed and the loading itself runs on the main Ractor (Ractor._requireexists 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.
# 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"raisesLoadError;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_PATHand, if it is not found there, taken relative to the working directory. -
With
wrapset totrue, 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.
# 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.
# 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)registersconst(a symbol or a string) in that module,Shelf.autoload(:Isbn, "shelf/isbn"). Inside amoduleorclassbody the receiver is implicit. -
Kernel#autoload(const, filename)registers the constant in the current lexical namespace (the module or class being defined, orObjectat 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 (ornilif there is none or the constant is already loaded), searching the ancestors unlessinheritisfalse. It does not load anything. -
Referencing the constant is what triggers the load.
defined?(Shelf::Isbn)andconst_defined?answer without loading;Module#constantslists autoload constants;Module#const_source_locationreturns the file and line of theautoloadcall until the constant has been loaded, and the file and line of the definition afterwards.
# 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:
# 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 |
|
Name the file after the constant it defines. This is the rule that Zeitwerk checks for you. |
The file does not exist |
|
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 |
|
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::TitleCasecan be autoloaded because callers write its name inusing, but a file that only monkey patchesStringmust 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
inheritedhook, aStructorDatadefinition and aModule#includedcallback 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
includereferences 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 |
|---|---|---|
|
|
Top-level file named after the namespace; the directory |
|
|
Directory = enclosing module; file = one constant. |
|
|
Nested namespaces are nested directories. |
|
|
The file name is the constant name in snake case ( |
|
|
A directory with Ruby files and no file of its own is an implicit namespace: Zeitwerk creates the module for you. |
|
|
The exception: an all-caps constant. Zeitwerk’s gem inflector expects exactly this file to define |
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).
# 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.
# 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_gemis the shortcut for a gem whose entry point islib/my_gem.rb: it manageslibas the root directory and expectslib/my_gem/version.rbto defineMyGem::VERSION. The entry point must be loaded byKernel#require(the way a gem is normally loaded), notloadorrequire_relative. -
The generic form is
Zeitwerk::Loader.newwithpush_dir;setupfinishes the configuration. -
eager_loadloads every managed file up front, and is the way a test suite checks that every file defines the constant its name promises: a violation raisesZeitwerk::NameError, a subclass ofNameError. -
enable_reloading(beforesetup) andreloadsupport 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.ignoreskips files that do not follow the convention;loader.collapsetreats 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.
# 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!
Source: Ruby 4.0 documentation: Ruby Box
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 |
Scope |
A box works at file scope: a |
Built-in classes |
Visible in every box and reopenable. A reopened |
Classes defined in a box |
Reached from outside as |
Constants, class instance and class variables, globals |
Separate per box, including the ones on built-in classes. A global (even |
Top-level methods |
Private instance methods of |
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 |
|
It does not depend on the working directory or the load path. |
Load the standard library or a gem |
|
Resolved through the load path or Bundler. |
Make |
|
|
Run a file again each time |
|
|
Isolate the names a file defines |
|
The file’s constants go into a module, not |
Defer the cost of a large dependency |
|
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 |
See why a require loaded the wrong file |
|
Shows the resolution without side effects. |
Keep one plugin’s monkey patches away from the rest |
A refinement; as an experiment, |
Refinements are stable and lexical; boxes are process-wide and experimental. |
-
requirewith a bare relative path does not search the working directory.require "lib/shelf"fails;require "./lib/shelf"works and is resolved againstDir.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 => edoes not catchLoadError. It is aScriptError. RescueLoadErrorexplicitly, and re-raise it if itspathis not the one you expected. -
Do not reassign
$LOAD_PATH. It is read-only; change the array in place. -
Top-level
autoloadregisters onObject. Insidemodule Shelf ... endit registers onShelf. Checkautoload?on the namespace you meant. -
Do not put a
requireof 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 |
|---|---|
|
RubyGems is loaded at start-up and wraps |
|
|
|
|
|
They are bundled gems (4.0: |
|
|
|
|
|
|
|
|
Load isolation by |
Available since forever, still works; |
Version notes
| Version | Change |
|---|---|
3.1 |
|
3.2 |
|
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 |
|
4.0 |
|
4.1 (preview) |
|
References
-
Kernel#require, Kernel#require_relative, Kernel#load and Kernel#autoload
-
Module#autoload, Module#autoload?, Module#const_missing and Module#const_source_location
-
Global variables (
$LOAD_PATH,$LOADED_FEATURES,$PROGRAM_NAME) and command-line options (-I,-r) -
LoadError and ScriptError
-
Ruby 3.1.0 NEWS, 3.4.0 and 4.0.0; Feature #15330 (
autoload_relative)