skip to content

In Ruby, how does require search $LOAD_PATH, and how does $LOADED_FEATURES enforce its once-only rule?

level: middleimportance: must knowfreq 42%

answer

  1. first matching directory wins
  2. $: and $-I are aliases
  3. absolute paths in $"
  4. failed loads are not recorded
  5. resolve_feature_path shows the winner

basics

~20 s

require tries each $LOAD_PATH directory in order and loads the first match. After a successful load it records the file's absolute path in $LOADED_FEATURES and skips any later require resolving to that same file; a load that raised is not recorded.

solid answer

~40 s

`$LOAD_PATH` (aliases `$:` and `$-I`) is an array of directories; `require "sitegen/page"` looks for `sitegen/page.rb` in each directory in order (a compiled extension only if no directory has the `.rb` file), and the first directory holding it wins, so an earlier directory can shadow a library of the same name. The variable itself is read-only, but the array can be changed with `unshift` or `push`. After the file runs without raising, its absolute path goes into `$LOADED_FEATURES` (`$"`), and a later `require` that resolves to the same real file, even through a different spelling or a symlink, returns `false`. If the file raised while loading, it is not recorded, so the next `require` runs it again. `$LOAD_PATH.resolve_feature_path("name")` shows which file would win without loading it.

code

ruby · 10 lines
ruby
$LOAD_PATH.unshift(File.expand_path("lib", __dir__))

$LOAD_PATH.resolve_feature_path("sitegen/page")
# => [:rb, "/srv/sitegen/lib/sitegen/page.rb"]

require "sitegen/page"                 # => true
$LOADED_FEATURES.grep(/sitegen\/page/) # => ["/srv/sitegen/lib/sitegen/page.rb"]
require "sitegen/page"                 # => false

$LOAD_PATH = []   # NameError: $: is a read-only variable

go deeper

for a junior

Recall that require looks in the directories of $LOAD_PATH and remembers loaded files in $LOADED_FEATURES.

for a middle

Explain the ordered search, first match wins, the absolute-path bookkeeping, and that a load which raised is not recorded and will run again.

for a senior

Diagnose shadowed libraries, stale code and circular requires with resolve_feature_path and $LOADED_FEATURES, and keep load-path changes to entry points and test helpers.

for a principal

Decide where load-path configuration lives: entry scripts and tool configuration, not library code, so every process resolves the same files the same way.

## Two global arrays behind every `require` | Global | Aliases | Holds | Writable? | |---|---|---|---| | `$LOAD_PATH` | `$:`, `$-I` | directories searched by `require` and `load` | the variable is read-only; the array is mutable | | `$LOADED_FEATURES` | `$"` | paths of files already loaded by `require` | the array is mutable | Assigning `$LOAD_PATH = [...]` raises `NameError` (it is a read-only variable). Changing its contents with `$LOAD_PATH.unshift(dir)` is allowed and is how RubyGems, Bundler and the `-I` switch add directories. ## How the search works For `require "sitegen/page"`: 1. If the name is absolute, or starts with `./` or `../`, Ruby uses it directly (the relative forms from `Dir.pwd`). 2. Otherwise it walks `$LOAD_PATH` **in order** looking for `sitegen/page.rb`; only if no directory has it does it repeat the walk for the platform's compiled-extension name. 3. The **first** directory that has a match wins; later directories are never consulted. 4. If nothing matches, `LoadError` is raised with `cannot load such file -- sitegen/page`. Because the first match wins, a directory prepended to `$LOAD_PATH` can **shadow** a library: a project file called `lib/json.rb` on a prepended `lib` directory is loaded in place of the standard `json`. `$LOAD_PATH.resolve_feature_path("json")` answers `[:rb, "/path/to/the/winner.rb"]` (or `[:so, ...]`, or `nil`) without loading anything, which makes shadowing easy to confirm. ## The once-only rule - After a file runs **without raising**, `require` appends its absolute path to `$LOADED_FEATURES`. - A later `require` that resolves to the same file returns `false`. Ruby also tracks real paths, so reaching the same file through a symlink or a different relative spelling does not run it again. - If the file **raised** while loading, it is not recorded. The next `require` of the same name runs it again from the top, including whatever part had already executed the first time, which can produce `already initialized constant` warnings. - Deleting an entry from `$LOADED_FEATURES` makes the next `require` load the file again. Code reloaders rely on this; application code should not. ## Concurrency and cycles - If two threads `require` the same file at once, one loads it while the other **waits**; when the first finishes, the waiting call returns `false` instead of loading it a second time. - If a file requires another file that is still in the middle of being loaded by the same thread (a **circular require**), the inner call returns `false` straight away. With `-w`, Ruby warns `loading in progress, circular require considered harmful`. The caller may then see constants that the partly loaded file has not defined yet. ## Where the directories come from - The interpreter's own library directories. - Directories added by RubyGems for activated gems, or by Bundler for the bundle. - The `RUBYLIB` environment variable and the `-I` switch, both placed ahead of the standard directories. - Explicit `$LOAD_PATH.unshift(File.expand_path("lib", __dir__))` in scripts and test helpers. ## Debugging checklist 1. `LoadError` for a file that exists: print `$LOAD_PATH` and check the directory is really there, spelled as an absolute path. 2. The wrong version of a file is loaded: `$LOAD_PATH.resolve_feature_path("name")` shows the winning path. 3. Edits are ignored: `$LOADED_FEATURES.grep(/name/)` shows the file is already loaded, so `require` will not run it again. ## Common mistakes - Pushing a relative directory (`$LOAD_PATH << "lib"`): it is resolved against the working directory at each lookup, so it breaks when the program starts elsewhere. Expand it with `File.expand_path("lib", __dir__)`. - Naming a project file after a standard library or gem feature (`logger.rb`, `json.rb`) at the top of a load-path directory. - Changing `$LOAD_PATH` from library code: every program that loads the library inherits the change. Keep it to entry scripts and test helpers. - Deleting entries from `$LOADED_FEATURES` to force a reload in application code, which re-runs top-level side effects and redefines constants.

  • A file raises halfway through its first `require`. What happens on the next `require` of it?
    It runs again from the top, because only a load that finishes without raising is recorded in `$LOADED_FEATURES`. Anything the first attempt already defined is defined again, so constants trigger `already initialized constant` warnings and registration side effects repeat.
  • How can a project file silently replace a standard library?
    If a directory placed early in `$LOAD_PATH` contains a file with the same feature name, such as `lib/json.rb`, `require "json"` finds it first and never looks further. `$LOAD_PATH.resolve_feature_path("json")` reveals which file wins; renaming or namespacing the project file (`lib/sitegen/json.rb`) removes the clash.

saying these in an interview costs you the question

  • require searches every directory and loads the newest matching file
  • A require that raised is still recorded as loaded
  • $LOAD_PATH can be reassigned to a new array
  • Requiring the same file through a symlink loads it a second time
  • Two threads requiring one file at once both run it